@kitn.ai/ui 0.25.2 → 0.27.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 +17 -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-b5A8hlHs.js → Icon-BnFKqqtY.js} +1 -1
- package/dist/action-icons-CBskfBMz.js +1 -0
- package/dist/arrow-down-Br0qtpsz.js +1 -0
- package/dist/arrow-left-Dj7xgE12.js +1 -0
- package/dist/artifact-CvXEm1lS.js +1 -0
- package/dist/assets/dev-5XzXYR21.js +91 -0
- package/dist/attachments-C43ZegEq.js +1 -0
- package/dist/audio-visualizer-B-79y1Tj.js +1 -0
- package/dist/badge-BpUbq8c0.js +1 -0
- package/dist/bash-InADTalH.js +1 -6
- package/dist/bell-sn17Ip9v.js +1 -0
- package/dist/{elements/chunks/button-B9okqG73.js → button-BH6rN0U-.js} +1 -1
- package/dist/card-renderer-Cb1gQmth.js +1 -0
- package/dist/card-routing-DifQgS7n.js +1 -0
- package/dist/check-DPeeMUYx.js +1 -0
- package/dist/checkbox-Dc9Yov_B.js +1 -0
- package/dist/checkbox-group-B1kDLLYk.js +1 -0
- package/dist/chevron-down-CHSWk7ZT.js +1 -0
- package/dist/chevron-right-B7l_fMgx.js +1 -0
- package/dist/choice-card-BT3c1S8g.js +1 -0
- package/dist/circle-Bb0yOpqM.js +1 -0
- package/dist/circle-check-BGZpCiz7.js +1 -0
- package/dist/circle-x-wobWkoSx.js +1 -0
- package/dist/{elements/chunks/cn-CU7UAWzk.js → cn-DN5AWfiS.js} +1 -1
- package/dist/code-block-CUQ5mtv0.js +1 -0
- package/dist/collapsible-CPJbf6FM.js +1 -0
- package/dist/components/attachment-types.d.ts +53 -1
- package/dist/components/attachments.d.ts +31 -0
- package/dist/components/audio-visualizer/fit-scale.d.ts +50 -0
- package/dist/components/audio-visualizer/index.d.ts +15 -1
- package/dist/components/audio-visualizer/variant-bar.d.ts +24 -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 +49 -42
- package/dist/components/code-block.d.ts +22 -0
- package/dist/components/conversation-item.d.ts +66 -1
- package/dist/components/conversation-list.d.ts +77 -0
- package/dist/components/form-widgets.d.ts +67 -2
- package/dist/components/form.d.ts +89 -0
- package/dist/components/message.d.ts +29 -0
- 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/toast.d.ts +2 -1
- package/dist/components/tool-types.d.ts +9 -0
- package/dist/components/voice-input.d.ts +8 -0
- package/dist/components/voice-output.d.ts +12 -1
- package/dist/components/workspace-shell.d.ts +87 -0
- package/dist/composer-CjngLzal.js +106 -0
- package/dist/confirm-card-DCx_A4bU.js +1 -0
- package/dist/construct-cli.es.js +1169 -0
- package/dist/context-BY8V-Nlp.js +1 -0
- package/dist/controllable-D43BKmtX.js +1 -0
- package/dist/conversation-list-D4lLU_yS.js +1 -0
- package/dist/copy-B6DO4PqB.js +1 -0
- package/dist/core-AYMC6_lb.js +12 -5874
- package/dist/{create-tween-CUzOlaMS.js → create-tween-BPzaefTp.js} +1 -1
- package/dist/{create-tween-B5Y4Im8G.js → create-tween-CnMHviKB.js} +1 -1
- package/dist/{create-tween-4z1XYLkR.js → create-tween-D1aiGZZh.js} +1 -1
- package/dist/{elements/chunks/create-tween-CUrM6D_o.js → create-tween-DUSQUKpr.js} +2 -2
- package/dist/css-M7EaDHN_.js +1 -6
- package/dist/custom-elements.json +3468 -1927
- package/dist/default-input-CucQEOqH.js +1 -0
- package/dist/define-CGGX-7Vg.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/diagnostics/hook.d.ts +81 -0
- package/dist/diagnostics/index.d.ts +7 -0
- package/dist/diagnostics/report-request.d.ts +61 -0
- package/dist/diagnostics.d.ts +5 -0
- package/dist/diagnostics.js +209 -0
- package/dist/disclosure-CbLtajUQ.js +1 -0
- package/dist/dropdown-BwhViJFd.js +1 -0
- 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.d.ts +1 -0
- package/dist/elements/conversation-item.js +1 -0
- package/dist/elements/conversation-list.js +1 -1
- package/dist/elements/default-input.d.ts +3 -3
- package/dist/elements/define-entry.d.ts +12 -0
- package/dist/elements/define.d.ts +27 -0
- package/dist/elements/diagnostic-events.d.ts +74 -0
- package/dist/elements/dialog.js +1 -1
- package/dist/elements/dock.d.ts +1 -0
- package/dist/elements/dock.js +243 -0
- package/dist/elements/dropdown.d.ts +1 -0
- package/dist/elements/dropdown.js +1 -0
- package/dist/elements/editable-label.js +1 -1
- package/dist/elements/element-data-types.d.ts +37 -0
- package/dist/elements/element-diagnostics.d.ts +81 -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/register.d.ts +2 -0
- package/dist/elements/remote.js +1 -1
- package/dist/elements/resizable.js +1 -1
- package/dist/elements/response-stream.js +3 -3
- 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 +36 -4
- 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 +589 -167
- package/dist/{elements/chunks/ellipsis-CVxqfsDZ.js → ellipsis-DFr4Vqr3.js} +1 -1
- package/dist/embed-5s_vU4J0.js +1 -0
- package/dist/engine-javascript-vq0WuIJl.js +14 -2516
- package/dist/{elements/chunks/external-link-DG2sRdQL.js → external-link-u9d85ORQ.js} +1 -1
- package/dist/file-text-CX7_x42o.js +1 -0
- package/dist/{elements/chunks/file-tree-yGWNK0Js.js → file-tree-D3kBjXI5.js} +1 -1
- package/dist/folder-V-YdIL8f.js +1 -0
- package/dist/form-BRNUMCN2.js +1 -0
- package/dist/github-dark-dimmed-DUshB20C.js +1 -4
- package/dist/github-light-JYsPkUQd.js +1 -4
- package/dist/hover-card-DaeiYrTA.js +1 -0
- package/dist/html-CPZ3oZQ7.js +1 -10
- package/dist/icon-BpixdZNq.js +1 -0
- package/dist/{elements/chunks/index-BCRhi84a.js → index-xu_pdcPi.js} +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +8473 -6470
- package/dist/index.server.js +6495 -4958
- package/dist/info-8mFsIjS3.js +1 -0
- package/dist/input-d1Yuu-hz.js +1 -0
- package/dist/javascript-C25yR2R2.js +1 -6
- package/dist/json-DxJze_jm.js +1 -6
- package/dist/kai-provider.es.js +40 -40
- package/dist/kai.es.js +1 -1
- package/dist/kbd-961zMPFB.js +1 -0
- package/dist/link-DlZZBGaR.js +1 -0
- package/dist/{elements/chunks/link-preview-Cf5KOP-i.js → link-preview-DIIbV2Sd.js} +1 -1
- package/dist/loader-YQszHFaW.js +1 -0
- package/dist/{elements/chunks/markdown-DbozyYqa.js → markdown-CVk1MPsj.js} +1 -1
- package/dist/mcp.es.js +3873 -544
- package/dist/message-CwKVEdZg.js +1 -0
- package/dist/message-DS8lzHc1.js +1 -0
- package/dist/message-circle-Xf8Q-vZp.js +1 -0
- package/dist/message-square-Cxnwgkns.js +1 -0
- package/dist/{elements/chunks/minimize-2-SegJ_oku.js → minimize-2-UEZmP2QT.js} +1 -1
- package/dist/{elements/chunks/model-switcher-D7JrFFHp.js → model-switcher-Kk1jEQ5t.js} +1 -1
- package/dist/{elements/chunks/overlay-CV7z2YuX.js → overlay-C1od6LOz.js} +1 -1
- package/dist/panel-right-ryET0yyn.js +1 -0
- package/dist/paperclip-BDNoZFJP.js +1 -0
- package/dist/play-OFIvYXUQ.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/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 +23 -0
- package/dist/primitives/use-speech-recognition.d.ts +8 -0
- package/dist/primitives/use-text-stream.d.ts +5 -0
- package/dist/progress-bar-BvYX0aoy.js +1 -0
- package/dist/prompt-suggestion-BRFu2lcu.js +1 -0
- package/dist/radio-DcL23cNJ.js +1 -0
- package/dist/react/index.d.ts +306 -283
- package/dist/react.js +309 -247
- package/dist/reasoning-Cackq4Rh.js +1 -0
- package/dist/register-impl-C7rENvPK.js +424 -0
- package/dist/{elements/chunks/resizable-Dpzjlvp4.js → resizable-Cp9HUYih.js} +1 -1
- package/dist/rotate-ccw-0DLRDHH5.js +1 -0
- package/dist/rotate-cw-DOB9IGcU.js +1 -0
- package/dist/schemas/form.schema.json +21 -1
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/tool-defs.d.ts +45 -0
- package/dist/schemas.js +398 -305
- package/dist/scroll-area-emVmWP4K.js +1 -0
- package/dist/scroll-button-CeP_gv1x.js +1 -0
- package/dist/select-CWmu00tS.js +1 -0
- package/dist/{elements/chunks/separator-DH2r_ZSZ.js → separator-CE6vly6z.js} +1 -1
- package/dist/{elements/chunks/settings-BFMOd6yB.js → settings-CE-78SvZ.js} +1 -1
- package/dist/{elements/chunks/settings-group-B3VZKIcz.js → settings-group-ZFWNxFQR.js} +1 -1
- package/dist/{elements/chunks/skeleton-N8BuWYmf.js → skeleton-BebdP4mf.js} +1 -1
- package/dist/slider-BAKRYUlz.js +1 -0
- package/dist/slots-DaNjX0rc.js +1 -0
- package/dist/{solid-BMuDmFo_.js → solid-BE5ui6sy.js} +10672 -8116
- package/dist/{solid-CGYNdLRR.js → solid-DFhsce3O.js} +8137 -6056
- package/dist/solid.d.ts +15 -0
- package/dist/solid.js +248 -235
- package/dist/solid.server.js +248 -235
- package/dist/source-B0FlguSx.js +1 -0
- package/dist/star-CBMZvLuN.js +1 -0
- package/dist/state/index.d.ts +5 -1
- package/dist/state/mock.d.ts +32 -2
- package/dist/state/persistence.d.ts +51 -0
- package/dist/state/stream.d.ts +50 -0
- package/dist/state/threads.d.ts +71 -0
- package/dist/state.js +297 -172
- package/dist/store-7eoadydQ.js +1 -0
- package/dist/svelte--5p79yCD.js +1 -15
- package/dist/switch-CViARZCy.js +1 -0
- package/dist/tasks-card-MjCcOhR_.js +1 -0
- package/dist/text-shimmer-DKlwRF2G.js +1 -0
- package/dist/textarea-3ay2Me2-.js +1 -0
- package/dist/theme.tokens.css +82 -13
- package/dist/{elements/chunks/thumbs-up-DQe0hvkZ.js → thumbs-up-DQcu2HMO.js} +1 -1
- package/dist/toast-store-C6yKOhbk.js +1 -0
- package/dist/{elements/chunks/tool-C1YtxwPu.js → tool-BCYNlfKy.js} +1 -1
- package/dist/{elements/chunks/tooltip-BqwnFWhn.js → tooltip-05zI4hjs.js} +1 -1
- package/dist/trash-2-BR1RxGky.js +1 -0
- package/dist/{elements/chunks/triangle-alert-B6or7F5u.js → triangle-alert-DHohDOCy.js} +1 -1
- package/dist/tsx-B8rCNbgL.js +1 -6
- package/dist/types.d.ts +4 -2
- 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 +131 -0
- package/dist/ui/hover-card.d.ts +44 -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-BbGW6A_u.js +1 -0
- package/dist/url-scheme-policy-DHpoTJwB.js +1 -0
- package/dist/use-card-resolution-CJW4grFa.js +1 -0
- package/dist/{variant-aurora-DwSOfraQ.js → variant-aurora-BUOVgPOB.js} +27 -27
- package/dist/{variant-aurora-wKvAQ5wp.js → variant-aurora-CUsUJvFI.js} +28 -28
- package/dist/{variant-aurora-XEkPT0_z.js → variant-aurora-CW2i4bfr.js} +2 -2
- package/dist/{elements/chunks/variant-aurora-QXTZPyST.js → variant-aurora-xiMPty8f.js} +2 -2
- package/dist/variant-custom-BQtOnmeO.js +1 -0
- package/dist/{variant-custom-BhS9wIpU.js → variant-custom-CAm9L-E5.js} +65 -65
- package/dist/{variant-custom-eFIg2XWQ.js → variant-custom-CAnzzppW.js} +40 -40
- package/dist/variant-custom-D91HjAKz.js +1 -0
- package/dist/{variant-wave-CUWGx9Bn.js → variant-wave-Ca1Ls4_y.js} +21 -21
- package/dist/{variant-wave-CbVsmgwT.js → variant-wave-CxX39DKd.js} +25 -25
- package/dist/{variant-wave-BwzrHD1s.js → variant-wave-DYyzIIZv.js} +2 -2
- package/dist/{elements/chunks/variant-wave-DDCwhLas.js → variant-wave-uY5crU_K.js} +2 -2
- package/dist/vue-BmIZj4XD.js +1 -33
- package/dist/wire/chunk.d.ts +47 -0
- package/dist/wire/diagnostics.d.ts +575 -0
- package/dist/wire/encode-probe.d.ts +70 -0
- package/dist/wire/encode.d.ts +24 -0
- package/dist/wire/index.d.ts +2 -0
- package/dist/wire/sse.d.ts +10 -2
- package/dist/wire.js +917 -480
- package/dist/x-C5m1JFgi.js +1 -0
- package/frameworks/react/index.tsx +325 -108
- package/llms-full.txt +1354 -82
- package/llms.txt +6 -3
- package/package.json +39 -6
- package/src/agent-tooling/archetypes.ts +41 -9
- package/src/agent-tooling/catalog/README.md +676 -0
- package/src/agent-tooling/catalog/catalog-types.ts +231 -0
- package/src/agent-tooling/catalog/fabrications.ts +96 -0
- package/src/agent-tooling/catalog/invariants.ts +284 -0
- package/src/agent-tooling/catalog/labs-titles.ts +114 -0
- package/src/agent-tooling/catalog/scenarios.ts +87 -0
- package/src/agent-tooling/catalog/surfaces.ts +347 -0
- package/src/agent-tooling/construct/cli-entry.ts +8 -0
- package/src/agent-tooling/construct/cli.ts +131 -0
- package/src/agent-tooling/construct/codegen.ts +1353 -0
- package/src/agent-tooling/construct/construct.v1.schema.json +250 -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 +23 -0
- package/src/agent-tooling/construct/schema.ts +371 -0
- package/src/agent-tooling/integrations/anthropic.ts +7 -1
- package/src/agent-tooling/integrations/cloudflare.ts +43 -2
- package/src/agent-tooling/integrations/langgraph.ts +7 -1
- package/src/agent-tooling/integrations/mastra.ts +7 -1
- package/src/agent-tooling/integrations/mock.ts +92 -17
- package/src/agent-tooling/integrations/ollama.ts +7 -1
- package/src/agent-tooling/integrations/openai.ts +7 -1
- package/src/agent-tooling/integrations/openrouter.ts +7 -1
- package/src/agent-tooling/integrations/pi.ts +56 -1
- package/src/agent-tooling/integrations/vercel-ai-sdk.ts +7 -1
- package/src/agent-tooling/mcp/css-raw.d.ts +12 -0
- package/src/agent-tooling/mcp/manifest.ts +100 -1
- package/src/agent-tooling/mcp/server.ts +63 -6
- 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 +542 -5
- package/src/agent-tooling/mcp/tools/scaffold.ts +1834 -307
- package/src/agent-tooling/mcp/tools/theme.ts +146 -30
- package/src/agent-tooling/mcp/validate-args.ts +141 -0
- 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 +24 -7
- package/src/agent-tooling/route-emit.ts +26 -2
- package/src/components/attachment-types.ts +54 -1
- package/src/components/attachments.tsx +169 -22
- package/src/components/audio-visualizer/fit-scale.ts +112 -0
- package/src/components/audio-visualizer/index.tsx +80 -3
- package/src/components/audio-visualizer/variant-aurora.tsx +10 -4
- package/src/components/audio-visualizer/variant-bar.tsx +38 -6
- package/src/components/audio-visualizer/variant-custom.tsx +8 -2
- package/src/components/audio-visualizer/variant-grid.tsx +16 -11
- package/src/components/audio-visualizer/variant-radial.tsx +15 -10
- package/src/components/audio-visualizer/variant-wave.tsx +7 -2
- package/src/components/card-renderer.tsx +39 -8
- package/src/components/card.tsx +10 -2
- package/src/components/chat-container.tsx +4 -1
- package/src/components/chat-scope-picker.tsx +2 -2
- package/src/components/chat-thread.tsx +105 -13
- package/src/components/choice-card.tsx +102 -37
- package/src/components/coachmark.tsx +1 -1
- package/src/components/code-block.tsx +81 -5
- package/src/components/composer.tsx +89 -8
- package/src/components/confirm-card.tsx +20 -4
- package/src/components/conversation-item.tsx +138 -4
- package/src/components/conversation-list.tsx +230 -15
- package/src/components/embed.tsx +4 -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 +336 -21
- package/src/components/loader.tsx +12 -5
- package/src/components/message-skills.tsx +1 -1
- package/src/components/message.tsx +142 -12
- 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/response-stream.tsx +13 -7
- package/src/components/screen.tsx +5 -0
- package/src/components/scroll-button.tsx +63 -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 +11 -5
- package/src/components/tool-types.ts +9 -0
- package/src/components/voice-input.tsx +35 -3
- package/src/components/voice-output.tsx +46 -6
- package/src/components/workspace-shell.tsx +285 -0
- package/src/diagnostics/hook.ts +352 -0
- package/src/diagnostics/index.ts +82 -0
- package/src/diagnostics/report-request.ts +296 -0
- package/src/elements/attachments.tsx +9 -8
- package/src/elements/audio-visualizer.tsx +48 -4
- package/src/elements/chat-workspace.tsx +159 -287
- package/src/elements/chat.tsx +31 -10
- package/src/elements/checkbox-group.tsx +164 -0
- package/src/elements/checkbox.tsx +134 -0
- package/src/elements/code-block.tsx +19 -1
- package/src/elements/command.tsx +4 -1
- package/src/elements/compiled.css +1 -1
- package/src/elements/composer.tsx +12 -1
- package/src/elements/conversation-item.tsx +120 -0
- package/src/elements/conversation-list.tsx +60 -11
- package/src/elements/default-input.tsx +5 -5
- package/src/elements/define-entry.ts +12 -0
- package/src/elements/define.tsx +168 -8
- package/src/elements/diagnostic-events.ts +114 -0
- package/src/elements/dialog.tsx +47 -0
- package/src/elements/disclosure.ts +34 -12
- package/src/elements/dock.tsx +215 -0
- package/src/elements/dropdown.tsx +123 -0
- package/src/elements/element-data-types.ts +40 -0
- package/src/elements/element-diagnostics.ts +392 -0
- package/src/elements/element-manifest.json +36 -0
- package/src/elements/element-meta.json +1483 -560
- package/src/elements/element-nonscalar.json +185 -0
- package/src/elements/element-types.d.ts +589 -167
- 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/menu.tsx +13 -0
- package/src/elements/message.tsx +2 -0
- package/src/elements/pane-grid.tsx +111 -0
- package/src/elements/prompt-dock.tsx +2 -1
- package/src/elements/prompt-input.tsx +16 -10
- package/src/elements/radio-group.tsx +129 -0
- package/src/elements/register-impl.ts +52 -0
- package/src/elements/register.ts +28 -0
- package/src/elements/resizable.tsx +52 -25
- package/src/elements/scroll-button.tsx +59 -6
- package/src/elements/select.tsx +165 -0
- package/src/elements/slider.tsx +171 -0
- package/src/elements/slots.ts +108 -13
- package/src/elements/styles.css +119 -9
- package/src/elements/thinking-bar.tsx +16 -7
- package/src/elements/thread.tsx +2 -0
- package/src/elements/toast.tsx +17 -1
- package/src/elements/tool.tsx +26 -5
- package/src/elements/voice-input.tsx +11 -2
- package/src/elements/voice-output.tsx +16 -3
- package/src/index.ts +4 -1
- 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/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 +33 -0
- package/src/primitives/use-speech-recognition.ts +12 -1
- package/src/primitives/use-stick-to-bottom.ts +34 -2
- package/src/primitives/use-text-stream.ts +33 -2
- package/src/remote/provider-runtime.ts +6 -3
- package/src/schemas/index.ts +1 -0
- package/src/schemas/tool-defs.ts +307 -6
- package/src/solid.ts +15 -0
- package/src/state/index.ts +13 -1
- package/src/state/mock.ts +92 -6
- package/src/state/persistence.ts +180 -0
- package/src/state/stream.ts +71 -4
- package/src/state/threads.ts +142 -0
- package/src/types.ts +4 -2
- 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/dialog.tsx +6 -0
- package/src/ui/dock.tsx +584 -0
- package/src/ui/dropdown.tsx +67 -23
- package/src/ui/hover-card.tsx +156 -6
- package/src/ui/icon.tsx +53 -0
- package/src/ui/input.tsx +275 -16
- 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/resizable.tsx +11 -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/src/wire/chunk.ts +47 -0
- package/src/wire/consume.ts +185 -26
- package/src/wire/diagnostics.ts +727 -0
- package/src/wire/encode-probe.ts +214 -0
- package/src/wire/encode.ts +267 -14
- package/src/wire/formats/anthropic.ts +8 -1
- package/src/wire/formats/openai.ts +5 -0
- package/src/wire/index.ts +22 -0
- package/src/wire/read.ts +250 -6
- package/src/wire/sse.ts +16 -3
- package/theme.css +84 -13
- package/dist/elements/chunks/action-icons-DW9muWrY.js +0 -1
- package/dist/elements/chunks/arrow-left-iJVnQmxA.js +0 -1
- package/dist/elements/chunks/artifact-BSuPYFHY.js +0 -1
- package/dist/elements/chunks/attachments-CL6TaqIb.js +0 -1
- package/dist/elements/chunks/audio-visualizer-BuUqQmaO.js +0 -1
- package/dist/elements/chunks/badge-TIMiJFKc.js +0 -1
- package/dist/elements/chunks/bash-InADTalH.js +0 -1
- package/dist/elements/chunks/card-renderer-CSoTL_e1.js +0 -1
- package/dist/elements/chunks/card-routing-BK09BdmL.js +0 -1
- package/dist/elements/chunks/chat-thread-DxoKaStk.js +0 -1
- package/dist/elements/chunks/check-MdLzbxrm.js +0 -1
- package/dist/elements/chunks/chevron-down-MK7IzDqe.js +0 -1
- package/dist/elements/chunks/chevron-right-C5jXhmtV.js +0 -1
- package/dist/elements/chunks/choice-card-CGMc3kek.js +0 -1
- package/dist/elements/chunks/circle-BG7VkPep.js +0 -1
- package/dist/elements/chunks/circle-check-GqvO2UBs.js +0 -1
- package/dist/elements/chunks/circle-x-mhH7pOK2.js +0 -1
- package/dist/elements/chunks/code-block-ClhQO9RW.js +0 -1
- package/dist/elements/chunks/collapsible-DBcBkFPh.js +0 -1
- package/dist/elements/chunks/composer-CZZmtYOT.js +0 -69
- package/dist/elements/chunks/confirm-card-BWjf2-xo.js +0 -1
- package/dist/elements/chunks/context-hKjNqiyQ.js +0 -1
- package/dist/elements/chunks/conversation-list-CsGJjSKG.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-B1Mem5wu.js +0 -1
- package/dist/elements/chunks/define-B_tXMYRm.js +0 -1
- package/dist/elements/chunks/disclosure-DAoYOKew.js +0 -1
- package/dist/elements/chunks/download-DnCyWFJE.js +0 -1
- package/dist/elements/chunks/dropdown-CmNna5lT.js +0 -1
- package/dist/elements/chunks/embed-CjAD2jSi.js +0 -1
- package/dist/elements/chunks/engine-javascript-vq0WuIJl.js +0 -141
- package/dist/elements/chunks/file-text-D0J9F6M7.js +0 -1
- package/dist/elements/chunks/folder-NV1pi_ud.js +0 -1
- package/dist/elements/chunks/form-Cp8Y00Wn.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-6KtXpLwT.js +0 -1
- package/dist/elements/chunks/html-CPZ3oZQ7.js +0 -1
- package/dist/elements/chunks/icon-97R8SR2p.js +0 -1
- package/dist/elements/chunks/info-C8r5Q6cc.js +0 -1
- package/dist/elements/chunks/input-DrseEjhp.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-OGJDUT-O.js +0 -1
- package/dist/elements/chunks/link-BdsfLB7b.js +0 -1
- package/dist/elements/chunks/loader-B5QzK_8_.js +0 -1
- package/dist/elements/chunks/message-CE4k-qaj.js +0 -1
- package/dist/elements/chunks/message-DtXKPqiR.js +0 -1
- package/dist/elements/chunks/message-square-C9ovmtOo.js +0 -1
- package/dist/elements/chunks/paperclip-Bg0ITU3b.js +0 -1
- package/dist/elements/chunks/progress-bar-D1_rzt6N.js +0 -1
- package/dist/elements/chunks/prompt-suggestion-D5Sm8xF1.js +0 -1
- package/dist/elements/chunks/reasoning-BrxQfpfI.js +0 -1
- package/dist/elements/chunks/rotate-cw-DzVDJOA1.js +0 -1
- package/dist/elements/chunks/scroll-area-93A1bbeG.js +0 -1
- package/dist/elements/chunks/scroll-button-CrTo7wm4.js +0 -1
- package/dist/elements/chunks/share-DgNbkJKY.js +0 -1
- package/dist/elements/chunks/slots-CIw9RlAe.js +0 -1
- package/dist/elements/chunks/source-w9V0NnBn.js +0 -1
- package/dist/elements/chunks/star-DHvACxtm.js +0 -1
- package/dist/elements/chunks/store-HIPGhNz9.js +0 -1
- package/dist/elements/chunks/svelte--5p79yCD.js +0 -1
- package/dist/elements/chunks/tasks-card-lPxoodlA.js +0 -1
- package/dist/elements/chunks/text-shimmer-BY_rI3s5.js +0 -1
- package/dist/elements/chunks/textarea-DPfuhswM.js +0 -1
- package/dist/elements/chunks/toast-store-VUZZBhzH.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-DJg32jGH.js +0 -1
- package/dist/elements/chunks/variant-custom-D5ZlmziF.js +0 -1
- package/dist/elements/chunks/video-Bb86fVYI.js +0 -1
- package/dist/elements/chunks/vue-BmIZj4XD.js +0 -1
- package/dist/elements/chunks/x-BOhjBeOd.js +0 -1
- package/dist/llms/llms-full.txt +0 -2834
- package/dist/llms/llms.txt +0 -156
- package/dist/primitives/card-validate-generator.testlib.d.ts +0 -20
- package/dist/register-impl-DGmGzrEM.js +0 -145
- package/dist/ui/stat.d.ts +0 -16
- package/dist/variant-custom-MkTNTycd.js +0 -1
- package/src/primitives/card-validate-generator.testlib.ts +0 -38
- 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
|
@@ -0,0 +1,1353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* construct → generated Solid mini-project. THE single generation path:
|
|
3
|
+
* kai dev, kai compile and kai eject all call generateProject — the preview IS
|
|
4
|
+
* the artifact (owner-picked option B; no interpreter to drift).
|
|
5
|
+
*
|
|
6
|
+
* Quality bar: the output is the EJECT artifact. Deterministic (no dates, no
|
|
7
|
+
* randomness, object keys emitted in fixed order), idiomatic, readable.
|
|
8
|
+
* Interior is pure Solid composing @kitn.ai/ui/solid; provider glue imports
|
|
9
|
+
* @kitn.ai/ui/state + /wire (never a hand-rolled SSE reader); the one
|
|
10
|
+
* defineWebComponent facade carries the tag, theme default and slots.
|
|
11
|
+
* Styling: kit components + inline styles only — defineWebComponent injects
|
|
12
|
+
* the compiled kit CSS into the shadow root, so the generated project needs no
|
|
13
|
+
* Tailwind, no CSS build, nothing.
|
|
14
|
+
*/
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from 'node:fs';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
import type { Construct } from './schema';
|
|
19
|
+
|
|
20
|
+
export interface GeneratedFile {
|
|
21
|
+
path: string;
|
|
22
|
+
code: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface GenerateOptions {
|
|
26
|
+
/** Dependency spec for @kitn.ai/ui in the generated package.json.
|
|
27
|
+
* Default: `^<this package's version>` (self-name resolution, mcp/server.ts pattern).
|
|
28
|
+
* The gates pass a local tarball path here. */
|
|
29
|
+
uiSpec?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function kitVersion(): string {
|
|
33
|
+
const require = createRequire(import.meta.url);
|
|
34
|
+
const pkg = require('@kitn.ai/ui/package.json') as { version: string };
|
|
35
|
+
return pkg.version;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const themeMode = (c: Construct): 'light' | 'dark' | 'auto' =>
|
|
39
|
+
c.theme?.mode === 'light' ? 'light' : c.theme?.mode === 'dark' ? 'dark' : 'auto';
|
|
40
|
+
|
|
41
|
+
// ── accent contrast (codegen-time, static) ─────────────────────────────────
|
|
42
|
+
// A light accent (e.g. yellow) paired with the kit's default near-white
|
|
43
|
+
// foreground is unreadable, so the construct's accent needs a matching
|
|
44
|
+
// --kai-color-primary-foreground computed at generation time (the accent is
|
|
45
|
+
// static per construct — no reason to compute this at runtime for every
|
|
46
|
+
// browser that can't do it natively). Parses only the numeric CSS color forms
|
|
47
|
+
// (#rgb/#rrggbb/#rrggbbaa, rgb()/rgba(), hsl()/hsla()) — named colors, var(),
|
|
48
|
+
// and anything else exotic are NOT guessed at; see resolveContrastForeground.
|
|
49
|
+
|
|
50
|
+
/** sRGB channel (0-255) -> linear-light value, per the WCAG relative
|
|
51
|
+
* luminance formula. */
|
|
52
|
+
function srgbChannelToLinear(c: number): number {
|
|
53
|
+
const cs = c / 255;
|
|
54
|
+
return cs <= 0.03928 ? cs / 12.92 : Math.pow((cs + 0.055) / 1.055, 2.4);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** WCAG relative luminance, 0 (black) to 1 (white). */
|
|
58
|
+
function relativeLuminance(r: number, g: number, b: number): number {
|
|
59
|
+
return 0.2126 * srgbChannelToLinear(r) + 0.7152 * srgbChannelToLinear(g) + 0.0722 * srgbChannelToLinear(b);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
|
63
|
+
const hue = ((h % 360) + 360) % 360;
|
|
64
|
+
const chroma = (1 - Math.abs(2 * l - 1)) * s;
|
|
65
|
+
const x = chroma * (1 - Math.abs(((hue / 60) % 2) - 1));
|
|
66
|
+
const m = l - chroma / 2;
|
|
67
|
+
const [r1, g1, b1] =
|
|
68
|
+
hue < 60 ? [chroma, x, 0]
|
|
69
|
+
: hue < 120 ? [x, chroma, 0]
|
|
70
|
+
: hue < 180 ? [0, chroma, x]
|
|
71
|
+
: hue < 240 ? [0, x, chroma]
|
|
72
|
+
: hue < 300 ? [x, 0, chroma]
|
|
73
|
+
: [chroma, 0, x];
|
|
74
|
+
return [Math.round((r1 + m) * 255), Math.round((g1 + m) * 255), Math.round((b1 + m) * 255)];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Parse the numeric CSS color forms only. Returns null for anything this
|
|
78
|
+
* can't resolve to concrete RGB without guessing (named colors, var(),
|
|
79
|
+
* color-mix(), oklch(), etc.) — the caller must decide loudly rather than
|
|
80
|
+
* silently picking a default for those. */
|
|
81
|
+
function parseAccentRgb(accent: string): [number, number, number] | null {
|
|
82
|
+
const s = accent.trim();
|
|
83
|
+
const hex3 = /^#([0-9a-fA-F]{3})$/.exec(s);
|
|
84
|
+
if (hex3) {
|
|
85
|
+
const [r, g, b] = hex3[1].split('').map((ch) => parseInt(ch + ch, 16));
|
|
86
|
+
return [r, g, b];
|
|
87
|
+
}
|
|
88
|
+
const hex6 = /^#([0-9a-fA-F]{6})([0-9a-fA-F]{2})?$/.exec(s);
|
|
89
|
+
if (hex6) {
|
|
90
|
+
const hex = hex6[1];
|
|
91
|
+
return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
|
|
92
|
+
}
|
|
93
|
+
const rgbFn = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*[\d.]+\s*)?\)$/.exec(s);
|
|
94
|
+
if (rgbFn) {
|
|
95
|
+
const [r, g, b] = [rgbFn[1], rgbFn[2], rgbFn[3]].map(Number);
|
|
96
|
+
return [r, g, b].every((v) => v >= 0 && v <= 255) ? [r, g, b] : null;
|
|
97
|
+
}
|
|
98
|
+
const hslFn = /^hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%\s*(?:,\s*[\d.]+\s*)?\)$/.exec(s);
|
|
99
|
+
if (hslFn) {
|
|
100
|
+
return hslToRgb(Number(hslFn[1]), Number(hslFn[2]) / 100, Number(hslFn[3]) / 100);
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The paired --kai-color-primary-foreground for a parseable accent, or null
|
|
107
|
+
* when the accent can't be resolved to concrete RGB without guessing.
|
|
108
|
+
*
|
|
109
|
+
* Threshold: white when it sits closer to white than to black on the WCAG
|
|
110
|
+
* relative-luminance scale (L <= 0.5), else black — equivalently "contrast
|
|
111
|
+
* against white (1 - L) >= contrast against black (L)". This is a deliberately
|
|
112
|
+
* simpler comparison than the full WCAG *contrast-ratio* formula (which adds
|
|
113
|
+
* a 0.05 offset to both sides): the ratio formula's asymmetric offset flips
|
|
114
|
+
* the choice for at least one real accent this codegen ships in its own demo
|
|
115
|
+
* fixture (#e91e63, L≈0.1915 — contrast-ratio picks black at 4.83:1 over
|
|
116
|
+
* white's 4.35:1, but a straight luminance-distance comparison, and every
|
|
117
|
+
* reference brand palette pairing that color with white text, picks white).
|
|
118
|
+
* Verified by direct computation, not assumed — see the "accent contrast"
|
|
119
|
+
* describe block in codegen.test.ts for the worked numbers.
|
|
120
|
+
*/
|
|
121
|
+
export function resolveContrastForeground(accent: string): '#000000' | '#ffffff' | null {
|
|
122
|
+
const rgb = parseAccentRgb(accent);
|
|
123
|
+
if (!rgb) return null;
|
|
124
|
+
const luminance = relativeLuminance(...rgb);
|
|
125
|
+
return luminance <= 0.5 ? '#ffffff' : '#000000';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Shallow-merge a model's card tool-call args onto a DECLARED form schema's
|
|
130
|
+
* field defaults — "model proposes, user confirms" (CD-1, owner ruling
|
|
131
|
+
* 2026-08-26). Only top-level keys the args and the schema BOTH name are
|
|
132
|
+
* touched; the schema's own field shape (title/type/widget/validation) is
|
|
133
|
+
* never altered, and a key the model sent that isn't a declared field is
|
|
134
|
+
* ignored (the construct's vocabulary wins, not the model's). One level deep
|
|
135
|
+
* only — a nested object field's own defaults are not recursed into; no
|
|
136
|
+
* evidence of need yet (vocabulary-on-evidence).
|
|
137
|
+
*
|
|
138
|
+
* This is the real, module-level version used by this file's own tests
|
|
139
|
+
* (imported directly — see codegen-cards.render.test.tsx). `emitCardsImport`
|
|
140
|
+
* below emits an equivalent function VERBATIM as a string into the
|
|
141
|
+
* construct's own generated App.tsx: that copy is construct-glue code the
|
|
142
|
+
* eject artifact must own standalone (same as every other piece of logic
|
|
143
|
+
* emitCardsImport/emitApplyCardTools already emit inline), not an import
|
|
144
|
+
* from the kit — so the two are kept in sync by hand, not by import. Keep
|
|
145
|
+
* them behaviorally identical if you change one.
|
|
146
|
+
*/
|
|
147
|
+
export function mergeToolArgsIntoFormDefaults(
|
|
148
|
+
schema: Record<string, unknown>,
|
|
149
|
+
args: Record<string, unknown>,
|
|
150
|
+
): Record<string, unknown> {
|
|
151
|
+
const declared = schema.properties;
|
|
152
|
+
if (!declared || typeof declared !== 'object') return schema;
|
|
153
|
+
const patched: Record<string, unknown> = {};
|
|
154
|
+
for (const [key, value] of Object.entries(args)) {
|
|
155
|
+
if (!(key in (declared as Record<string, unknown>))) continue;
|
|
156
|
+
patched[key] = {
|
|
157
|
+
...((declared as Record<string, unknown>)[key] as Record<string, unknown>),
|
|
158
|
+
default: value,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return { ...schema, properties: { ...(declared as Record<string, unknown>), ...patched } };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Neutralize characters that could break out of a comment before embedding
|
|
166
|
+
* untrusted text in one. Two contexts reuse this: the CLI/dev notice line
|
|
167
|
+
* (plain terminal text — newlines just garble it) and the CSS NOTICE comment
|
|
168
|
+
* emitted into element.tsx, where the two-character close-comment sequence in
|
|
169
|
+
* the accent would otherwise end the comment early and let the rest of the
|
|
170
|
+
* accent's text land as live CSS inside the generated stylesheet.
|
|
171
|
+
*/
|
|
172
|
+
function commentSafe(text: string): string {
|
|
173
|
+
return text.replace(/[\r\n]/g, ' ').replace(/\*\//g, '* /');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const CONTRAST_COLOR_SUPPORTS = '@supports (color: contrast-color(red))';
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* One line for whichever host decides loudly about generation-time notices
|
|
180
|
+
* (the CLI today; `dev`'s watch loop reuses it on every regen). Null when
|
|
181
|
+
* there's nothing to say — no accent, or the accent parsed fine.
|
|
182
|
+
*/
|
|
183
|
+
export function accentContrastNotice(construct: Construct): string | null {
|
|
184
|
+
const accent = construct.theme?.accent;
|
|
185
|
+
if (!accent || resolveContrastForeground(accent) !== null) return null;
|
|
186
|
+
return `accent '${commentSafe(accent)}' not parseable for contrast; foreground left at theme default in browsers without CSS contrast-color() support`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function generateProject(construct: Construct, opts: GenerateOptions = {}): GeneratedFile[] {
|
|
190
|
+
const uiSpec = opts.uiSpec ?? `^${kitVersion()}`;
|
|
191
|
+
const files: GeneratedFile[] = [
|
|
192
|
+
{ path: 'package.json', code: emitPackageJson(construct, uiSpec) },
|
|
193
|
+
{ path: 'tsconfig.json', code: emitTsconfig() },
|
|
194
|
+
{ path: 'vite.config.ts', code: emitViteDev() },
|
|
195
|
+
{ path: 'vite.config.lib.ts', code: emitViteLib(construct) },
|
|
196
|
+
{ path: 'index.html', code: emitIndexHtml(construct) },
|
|
197
|
+
{ path: 'src/element.tsx', code: emitElement(construct) },
|
|
198
|
+
{ path: 'src/App.tsx', code: emitApp(construct) },
|
|
199
|
+
];
|
|
200
|
+
if (construct.cards) files.push({ path: 'src/cards.ts', code: emitCardsRegistry(construct.cards) });
|
|
201
|
+
return files;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── cards ────────────────────────────────────────────────────────────────
|
|
205
|
+
// Named generative-UI card definitions the model can emit as tool calls.
|
|
206
|
+
// Registration only — the projection into provider tool definitions is the
|
|
207
|
+
// kit's OWN `cardTools`/`toOpenAITools`/`toAnthropicTools` (@kitn.ai/ui/schemas,
|
|
208
|
+
// src/schemas/tool-defs.ts), never a second one authored here.
|
|
209
|
+
|
|
210
|
+
/** `src/cards.ts` — the construct's card registry, verbatim from the
|
|
211
|
+
* construct. Each schema is `JSON.stringify(schema, null, 2)`'d and reindented
|
|
212
|
+
* under its key — deterministic because the construct's own key order (and
|
|
213
|
+
* each schema's own JSON key order) is preserved; nothing here re-sorts. */
|
|
214
|
+
function emitCardsRegistry(cards: NonNullable<Construct['cards']>): string {
|
|
215
|
+
const entries = cards
|
|
216
|
+
.map((card) => ` ${card.name}: ${JSON.stringify(card.schema, null, 2).split('\n').join('\n ')},`)
|
|
217
|
+
.join('\n');
|
|
218
|
+
return `// src/cards.ts — the construct's card registry, verbatim from the construct.
|
|
219
|
+
// Tool definitions for YOUR backend derive from this same object via
|
|
220
|
+
// @kitn.ai/ui/schemas (cardTools / toOpenAITools / toAnthropicTools) — one
|
|
221
|
+
// projection, shared with the kit.
|
|
222
|
+
export const cards = {
|
|
223
|
+
${entries}
|
|
224
|
+
} as const;
|
|
225
|
+
`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** `, BUILTIN_CARD_COMPONENTS` spliced onto the `@kitn.ai/ui/solid` named-import
|
|
229
|
+
* list at the top of App.tsx (below) when cards are declared — the built-in
|
|
230
|
+
* `.form` renderer every declared card routes to. Empty otherwise. */
|
|
231
|
+
function emitCardComponentImport(c: Construct): string {
|
|
232
|
+
return c.cards ? ', BUILTIN_CARD_COMPONENTS' : '';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The `import`s cards need in App.tsx: the registry itself, `BUILTIN_CARD_COMPONENTS`
|
|
236
|
+
* (@kitn.ai/ui/solid re-exports it from the root entry) so every declared card can
|
|
237
|
+
* route to the kit's own schema-driven form renderer, `cardFromToolCall` (turns a
|
|
238
|
+
* settled tool-call ToolPart into a renderable `card` MessagePart — see
|
|
239
|
+
* emitApplyCardTools) and, for an endpoint construct, the wire-matching tool
|
|
240
|
+
* projection for the fetch body. Empty when the construct declares no cards at all
|
|
241
|
+
* (format rule: undeclared -> no affordance, no import).
|
|
242
|
+
*
|
|
243
|
+
* RULING (supervisor, this task): v1 renders EVERY declared card as the kit's own
|
|
244
|
+
* `form` card (`BUILTIN_CARD_COMPONENTS.form`, components/form.tsx) — it walks a
|
|
245
|
+
* JSON-Schema-shaped `data` into real input fields and honors `x-kai-format`/
|
|
246
|
+
* `x-kai-mask`/`x-kai-mask-guide` hints itself (field-mask.ts); no engine work is
|
|
247
|
+
* needed for masks specifically. This is deliberately NOT the same precedent as
|
|
248
|
+
* examples/apps/ops-console/shared/cards.ts's `createCardRegistry` (which maps
|
|
249
|
+
* several DISTINCT built-in kinds — confirm/form/choice/tasks — onto an app's own
|
|
250
|
+
* tool names): a construct's `cards` field carries only a `schema`, no `kind`, so
|
|
251
|
+
* there is no vocabulary yet to route on. Adding a `kind`/`type` field to pick
|
|
252
|
+
* confirm/choice/tasks is explicitly deferred to vocabulary-on-evidence, not done
|
|
253
|
+
* here — every construct card is a form until a later task adds that field. */
|
|
254
|
+
function emitCardsImport(c: Construct): string {
|
|
255
|
+
if (!c.cards) return '';
|
|
256
|
+
const toolsImport =
|
|
257
|
+
c.provider.mode === 'endpoint' ? (c.provider.wire === 'openai' ? ', toOpenAITools' : ', toAnthropicTools') : '';
|
|
258
|
+
return `import { cards } from './cards';
|
|
259
|
+
// Generative-UI cards, v1: every declared card renders as the kit's own
|
|
260
|
+
// schema-driven FORM (BUILTIN_CARD_COMPONENTS.form, components/form.tsx) — it
|
|
261
|
+
// walks the card's JSON Schema into real input fields, honoring
|
|
262
|
+
// x-kai-format/x-kai-mask/x-kai-mask-guide hints itself. ChatThread's own
|
|
263
|
+
// MessageBody already matches \`part.type === 'card'\` in its part rendering and
|
|
264
|
+
// draws it with the kit's own \`CardRenderer\` (components/card-renderer.tsx),
|
|
265
|
+
// which picks the component from \`cardTypes\` (below) by envelope.type — so
|
|
266
|
+
// there is nothing to hand-compose beyond that one map. Turning a model's tool
|
|
267
|
+
// call into that renderable part is \`cardFromToolCall\` (the inverse of
|
|
268
|
+
// \`cardTools\`), applied once per settled turn below; its data is then replaced
|
|
269
|
+
// with the DECLARED card schema (not the model's call arguments) — the fields
|
|
270
|
+
// on screen are the construct's own vocabulary, not whatever shape a model
|
|
271
|
+
// happened to send.
|
|
272
|
+
//
|
|
273
|
+
// UPDATE (CD-1, owner ruling 2026-08-26, Task 19g): the field SHAPE (title/
|
|
274
|
+
// type/widget/validation) still comes from the construct's own declared
|
|
275
|
+
// schema, never the model's — that part is unchanged. But discarding the
|
|
276
|
+
// model's call arguments wholesale also threw away any VALUE it wanted to
|
|
277
|
+
// pre-fill, breaking "model proposes, user confirms" (kai_refund_approval
|
|
278
|
+
// {amount:50} rendered an empty form). So the model's args are now
|
|
279
|
+
// shallow-merged onto the declared schema's field \`default\`s below
|
|
280
|
+
// (mergeToolArgsIntoFormDefaults) before the card is added — see
|
|
281
|
+
// emitApplyCardTools.
|
|
282
|
+
import { cardFromToolCall${toolsImport} } from '@kitn.ai/ui/schemas';
|
|
283
|
+
|
|
284
|
+
// Every declared card name routes to the SAME form renderer — cardFromToolCall
|
|
285
|
+
// makes envelope.type equal the card's own name (kai_refund_approval ->
|
|
286
|
+
// 'refund_approval'), and CardRenderer resolves a type's component from this
|
|
287
|
+
// map.
|
|
288
|
+
//
|
|
289
|
+
// Deliberately NOT also wiring ChatThread's \`cardSchemas\` prop to this
|
|
290
|
+
// registry below. That prop validates envelope.data AGAINST the named schema,
|
|
291
|
+
// and this card's data IS \`cards[name]\` itself (see emitApplyCardTools) — the
|
|
292
|
+
// construct's declared field schema, not values shaped like it. Wiring it as
|
|
293
|
+
// its own validator asks "does this FormDefinition itself have an \`amount\`
|
|
294
|
+
// key" and a well-formed FormDefinition never does, so every card would
|
|
295
|
+
// render the HARD validation-failure fallback instead of the form (caught
|
|
296
|
+
// live: eject + kai dev showed exactly that "(root).amount: required"
|
|
297
|
+
// failure before this comment existed). The construct's own schema.ts
|
|
298
|
+
// already checks \`cards\` structurally at validate time; there is nothing
|
|
299
|
+
// left for a second, self-referential check here to catch.
|
|
300
|
+
const cardTypes = Object.fromEntries(Object.keys(cards).map((name) => [name, BUILTIN_CARD_COMPONENTS.form] as const));
|
|
301
|
+
|
|
302
|
+
// CD-1 (owner ruling 2026-08-26, Task 19g): shallow-merge a model's card
|
|
303
|
+
// tool-call args onto a DECLARED form schema's field defaults — "model
|
|
304
|
+
// proposes, user confirms". Only top-level keys the args AND the schema both
|
|
305
|
+
// name are touched; the schema's own field shape (title/type/widget/
|
|
306
|
+
// validation) is never altered, and a key the model sent that isn't a
|
|
307
|
+
// declared field is ignored (the construct's vocabulary wins, not the
|
|
308
|
+
// model's). One level deep only — a nested object field's own defaults are
|
|
309
|
+
// not recursed into; no evidence of need yet (vocabulary-on-evidence).
|
|
310
|
+
function mergeToolArgsIntoFormDefaults(
|
|
311
|
+
schema: Record<string, unknown> & { properties?: Record<string, unknown> },
|
|
312
|
+
args: Record<string, unknown>,
|
|
313
|
+
): Record<string, unknown> {
|
|
314
|
+
const declared = schema.properties;
|
|
315
|
+
if (!declared || typeof declared !== 'object') return schema;
|
|
316
|
+
const patched: Record<string, unknown> = {};
|
|
317
|
+
for (const [key, value] of Object.entries(args)) {
|
|
318
|
+
if (!(key in declared)) continue;
|
|
319
|
+
patched[key] = { ...(declared[key] as Record<string, unknown>), default: value };
|
|
320
|
+
}
|
|
321
|
+
return { ...schema, properties: { ...schema.properties, ...patched } };
|
|
322
|
+
}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** `cardTypes={cardTypes}` on ChatThread — which component draws each
|
|
326
|
+
* declared card name (see emitCardsImport for why `cardSchemas` is
|
|
327
|
+
* deliberately NOT also registered). The host to emit card events off is
|
|
328
|
+
* already supplied by the ChatThread/kai-chat path (F-26); nothing else to
|
|
329
|
+
* thread through here. */
|
|
330
|
+
function emitCardTypesProp(c: Construct): string {
|
|
331
|
+
return c.cards ? ' cardTypes={cardTypes}' : '';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Settle a turn's tool calls into cards, called once after the read
|
|
335
|
+
* resolves and before `stream.done()`/`stream.abort()`. `AssistantStream`
|
|
336
|
+
* has no getter of its own, so the just-written parts are read back off
|
|
337
|
+
* `chat.messages()` by the stream's own id — the same pattern the kit's own
|
|
338
|
+
* `cardFromToolCall` doc comment (schemas/from-tool-call.ts) shows for a
|
|
339
|
+
* tool loop. A `kai_`-prefixed call becomes a card; anything else is the
|
|
340
|
+
* construct's own tool and is left as a plain `tool` part.
|
|
341
|
+
*
|
|
342
|
+
* `cardFromToolCall` supplies the envelope's `type`/`id` and `data` (the
|
|
343
|
+
* model's raw tool-call `input`, verbatim). `data` is then set to the card's
|
|
344
|
+
* own DECLARED schema off the registry (`cards[card.type]`) — the form
|
|
345
|
+
* renders the construct author's fields, matching the supervisor ruling
|
|
346
|
+
* that every declared card is a schema-driven form in v1 — with the
|
|
347
|
+
* model's args (`card.data`, read before this replaces it) shallow-merged
|
|
348
|
+
* onto that schema's field `default`s (CD-1, owner ruling 2026-08-26,
|
|
349
|
+
* Task 19g: `mergeToolArgsIntoFormDefaults`, emitted above by
|
|
350
|
+
* emitCardsImport) so "model proposes, user confirms" pre-fills the form
|
|
351
|
+
* instead of discarding the model's values outright. The `card.type in
|
|
352
|
+
* cards` guard only fires for a `kai_` call this construct never declared
|
|
353
|
+
* (an off-vocabulary call slipping through); it is silently dropped rather
|
|
354
|
+
* than rendered, matching cardFromToolCall's own "not every kai_ call is
|
|
355
|
+
* renderable" boundary (see its module header). */
|
|
356
|
+
function emitApplyCardTools(c: Construct): string {
|
|
357
|
+
if (!c.cards) return '';
|
|
358
|
+
return `
|
|
359
|
+
for (const part of chat.messages().find((m) => m.id === stream.id)?.parts ?? []) {
|
|
360
|
+
if (part.type !== 'tool' || part.tool.state !== 'input-available') continue;
|
|
361
|
+
const card = cardFromToolCall(part.tool.type, part.tool.input, { id: part.tool.toolCallId ?? crypto.randomUUID() });
|
|
362
|
+
if (card && card.type in cards) {
|
|
363
|
+
const declared = cards[card.type as keyof typeof cards];
|
|
364
|
+
const args = card.data as Record<string, unknown> | undefined;
|
|
365
|
+
const merged = args && typeof args === 'object'
|
|
366
|
+
? mergeToolArgsIntoFormDefaults(declared, args)
|
|
367
|
+
: declared;
|
|
368
|
+
stream.addCard({ ...card, data: merged });
|
|
369
|
+
}
|
|
370
|
+
}`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** The endpoint fetch body's `tools` field — the projected tool defs for
|
|
374
|
+
* every declared card, matching the construct's own wire. No cards, no
|
|
375
|
+
* field: the format rule (undeclared capability's affordance is OFF) holds
|
|
376
|
+
* for tools the same way it holds for suggestions/attach/reasoning above. */
|
|
377
|
+
function emitToolsField(c: Construct): string {
|
|
378
|
+
if (!c.cards || c.provider.mode !== 'endpoint') return '';
|
|
379
|
+
const toolsFn = c.provider.wire === 'openai' ? 'toOpenAITools' : 'toAnthropicTools';
|
|
380
|
+
return `, tools: ${toolsFn}(cards)`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function emitPackageJson(c: Construct, uiSpec: string): string {
|
|
384
|
+
return `${JSON.stringify(
|
|
385
|
+
{
|
|
386
|
+
name: c.name,
|
|
387
|
+
private: true,
|
|
388
|
+
type: 'module',
|
|
389
|
+
scripts: {
|
|
390
|
+
dev: 'vite',
|
|
391
|
+
build: 'vite build --config vite.config.lib.ts',
|
|
392
|
+
typecheck: 'tsc --noEmit',
|
|
393
|
+
},
|
|
394
|
+
dependencies: {
|
|
395
|
+
'@kitn.ai/ui': uiSpec,
|
|
396
|
+
'solid-js': '^1.9.0',
|
|
397
|
+
},
|
|
398
|
+
devDependencies: {
|
|
399
|
+
typescript: '^5.6.0',
|
|
400
|
+
vite: '^6.0.0',
|
|
401
|
+
'vite-plugin-solid': '^2.11.0',
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
null,
|
|
405
|
+
2,
|
|
406
|
+
)}\n`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function emitTsconfig(): string {
|
|
410
|
+
return `${JSON.stringify(
|
|
411
|
+
{
|
|
412
|
+
compilerOptions: {
|
|
413
|
+
target: 'ES2022',
|
|
414
|
+
module: 'ESNext',
|
|
415
|
+
moduleResolution: 'bundler',
|
|
416
|
+
jsx: 'preserve',
|
|
417
|
+
jsxImportSource: 'solid-js',
|
|
418
|
+
strict: true,
|
|
419
|
+
noUnusedLocals: true,
|
|
420
|
+
skipLibCheck: true,
|
|
421
|
+
types: ['vite/client'],
|
|
422
|
+
},
|
|
423
|
+
include: ['src'],
|
|
424
|
+
},
|
|
425
|
+
null,
|
|
426
|
+
2,
|
|
427
|
+
)}\n`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function emitViteDev(): string {
|
|
431
|
+
return `import { defineConfig } from 'vite';
|
|
432
|
+
import solid from 'vite-plugin-solid';
|
|
433
|
+
|
|
434
|
+
export default defineConfig({ plugins: [solid()] });
|
|
435
|
+
`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function emitViteLib(c: Construct): string {
|
|
439
|
+
return `import { defineConfig } from 'vite';
|
|
440
|
+
import solid from 'vite-plugin-solid';
|
|
441
|
+
|
|
442
|
+
// kai compile: ONE self-registering .js. Everything is inlined (no externals):
|
|
443
|
+
// the consumer installs nothing but this output.
|
|
444
|
+
export default defineConfig({
|
|
445
|
+
plugins: [solid()],
|
|
446
|
+
build: {
|
|
447
|
+
lib: { entry: 'src/element.tsx', formats: ['es'], fileName: () => '${c.name}.js' },
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function emitIndexHtml(c: Construct): string {
|
|
454
|
+
// A demo host page, not the emitted widget: purely so a first-time preview
|
|
455
|
+
// isn't a mystery blank tab with one small launcher in the corner. Outside
|
|
456
|
+
// the custom element entirely (a sibling in <body>), inline-styled, and
|
|
457
|
+
// worded so nobody mistakes it for the construct's own output. Keyed off
|
|
458
|
+
// `layout`: the "bottom-right corner" wording is only true for `widget` (a
|
|
459
|
+
// floating launcher) — `fullscreen`/`aside`/`split` (Task 12) fill or dock
|
|
460
|
+
// the page themselves, so they get no hint at all rather than a wrong one.
|
|
461
|
+
// Decide loudly by omission, not by a stale claim.
|
|
462
|
+
const hint =
|
|
463
|
+
c.layout === 'widget'
|
|
464
|
+
? `\n <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>`
|
|
465
|
+
: '';
|
|
466
|
+
// Task 13: when slots are declared, project real demo content into each one
|
|
467
|
+
// in the copy's own <${c.name}> tag — so the preview shows the escape hatch
|
|
468
|
+
// WORKING (light-DOM children of a custom element project into its shadow
|
|
469
|
+
// <slot> natively) rather than a mystery about how to use it. Slot names are
|
|
470
|
+
// already schema-validated to `^[a-z][a-z0-9-]*$` (schema.ts) — no
|
|
471
|
+
// quote/backslash payload possible, so no HTML-escaping is needed here,
|
|
472
|
+
// unlike a free-text construct-authored field.
|
|
473
|
+
const slotDemo = (c.slots ?? [])
|
|
474
|
+
.map(
|
|
475
|
+
(name) =>
|
|
476
|
+
`\n <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>`,
|
|
477
|
+
)
|
|
478
|
+
.join('');
|
|
479
|
+
const body = slotDemo ? `\n <${c.name}>${slotDemo}\n </${c.name}>` : `\n <${c.name}></${c.name}>`;
|
|
480
|
+
return `<!doctype html>
|
|
481
|
+
<html lang="en">
|
|
482
|
+
<head>
|
|
483
|
+
<meta charset="utf-8" />
|
|
484
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
485
|
+
<title>${c.name} — construct preview</title>
|
|
486
|
+
</head>
|
|
487
|
+
<body style="margin: 0;">${hint}${body}
|
|
488
|
+
<script type="module" src="/src/element.tsx"></script>
|
|
489
|
+
</body>
|
|
490
|
+
</html>
|
|
491
|
+
`;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function emitElement(c: Construct): string {
|
|
495
|
+
const accent = c.theme?.accent;
|
|
496
|
+
if (!accent) {
|
|
497
|
+
// `empty` (Task 14) composes straight into ChatThread's own `emptyContent`
|
|
498
|
+
// prop now (see emitEmptyContentProp's doc) — a plain JSX value passed down
|
|
499
|
+
// through App, not a Portal onto the host — so the facade needs no `ctx` and
|
|
500
|
+
// every construct, `empty` declared or not, keeps this line byte-for-byte
|
|
501
|
+
// unchanged.
|
|
502
|
+
const facade = '() => <App />';
|
|
503
|
+
return `import { defineWebComponent } from '@kitn.ai/ui/define';
|
|
504
|
+
import { App } from './App';
|
|
505
|
+
|
|
506
|
+
// The one facade. Interior stays pure Solid (no nested element registrations);
|
|
507
|
+
// the kit CSS is injected into the shadow root by defineWebComponent itself.
|
|
508
|
+
defineWebComponent('${c.name}', { theme: '${themeMode(c)}' as 'light' | 'dark' | 'auto' }, ${facade});
|
|
509
|
+
`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// The accent has to land on the HOST element, not anywhere inside this
|
|
513
|
+
// shadow root. The kit's --color-primary token is resolved ONCE, by a rule
|
|
514
|
+
// scoped to `:root, :host` (`@layer theme { :root, :host { --color-primary:
|
|
515
|
+
// var(--kai-color-primary, <fallback>) } }`) — so --kai-color-primary has to
|
|
516
|
+
// be set AT the host for that rule to see it; a descendant inside the
|
|
517
|
+
// shadow tree can set --kai-color-primary on itself all day and it will
|
|
518
|
+
// never flow back up into a value the :host rule already resolved. This is
|
|
519
|
+
// why the theme accent rendered nowhere in the T5 demo despite being
|
|
520
|
+
// "wired": an earlier version set it on a div INSIDE App's own render.
|
|
521
|
+
//
|
|
522
|
+
// `ctx.element` (the facade's second argument) IS the host, so this sets it
|
|
523
|
+
// from the one place inside the shadow root that has a handle to it.
|
|
524
|
+
// `style.setProperty` is also the safe way to carry the accent, distinct
|
|
525
|
+
// from string-interpolating it into CSS text: a custom property's value is
|
|
526
|
+
// an opaque token substituted via var(), so it can never break out into a
|
|
527
|
+
// new declaration or rule the way raw CSS/JS text interpolation could.
|
|
528
|
+
//
|
|
529
|
+
// The PAIRED --kai-color-primary-foreground is a separate concern: a light
|
|
530
|
+
// accent (yellow) next to the kit's default near-white foreground is
|
|
531
|
+
// unreadable. That pairing DOES need to live in a CSS block rather than a
|
|
532
|
+
// second setProperty call, because it has two layers that must be able to
|
|
533
|
+
// override each other in order: (1) a black/white value computed HERE, at
|
|
534
|
+
// generation time (the accent is static per construct), as the floor for
|
|
535
|
+
// browsers without CSS contrast-color() (Baseline Newly Available April
|
|
536
|
+
// 2026 — Chrome/Edge 147, Firefox 146, Safari 26; Widely Available is not
|
|
537
|
+
// until ~2028, and this widget embeds in arbitrary sites), and (2) inside
|
|
538
|
+
// `@supports (color: contrast-color(red))`, the NATIVE answer
|
|
539
|
+
// (contrast-color(var(--kai-color-primary))), which wins where supported —
|
|
540
|
+
// including for an accent this codegen couldn't parse. An inline
|
|
541
|
+
// `style.setProperty` always beats a stylesheet rule (short of
|
|
542
|
+
// `!important`), so achieving "the native answer wins where present" needs
|
|
543
|
+
// both layers to be plain `:host {}` declarations in one stylesheet, base
|
|
544
|
+
// rule first, @supports override after — ordinary cascade order, no
|
|
545
|
+
// `!important` required.
|
|
546
|
+
const foreground = resolveContrastForeground(accent);
|
|
547
|
+
const foregroundCss =
|
|
548
|
+
foreground !== null
|
|
549
|
+
? `:host { --kai-color-primary-foreground: ${foreground}; }\n`
|
|
550
|
+
: // Not guessed at: an unparseable accent (var(), a named color, a
|
|
551
|
+
// color-mix()/oklch() call, …) leaves NO base declaration, so the
|
|
552
|
+
// kit's own theme default stands — except in a browser new enough to
|
|
553
|
+
// resolve contrast-color() itself, where the @supports block below
|
|
554
|
+
// still gets it right natively.
|
|
555
|
+
`/* 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. */\n`;
|
|
556
|
+
const styleText =
|
|
557
|
+
foregroundCss + `${CONTRAST_COLOR_SUPPORTS} {\n :host { --kai-color-primary-foreground: contrast-color(var(--kai-color-primary)); }\n}`;
|
|
558
|
+
|
|
559
|
+
const facade = `(_props, ctx) => {
|
|
560
|
+
ctx.element.style.setProperty('--kai-color-primary', ${JSON.stringify(accent)});
|
|
561
|
+
return (
|
|
562
|
+
<>
|
|
563
|
+
<style>{${JSON.stringify(styleText)}}</style>
|
|
564
|
+
<App />
|
|
565
|
+
</>
|
|
566
|
+
);
|
|
567
|
+
}`;
|
|
568
|
+
return `import { defineWebComponent } from '@kitn.ai/ui/define';
|
|
569
|
+
import { App } from './App';
|
|
570
|
+
|
|
571
|
+
// The one facade. Interior stays pure Solid (no nested element registrations);
|
|
572
|
+
// the kit CSS is injected into the shadow root by defineWebComponent itself.
|
|
573
|
+
defineWebComponent('${c.name}', { theme: '${themeMode(c)}' as 'light' | 'dark' | 'auto' }, ${facade});
|
|
574
|
+
`;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ── App interior ─────────────────────────────────────────────────────────────
|
|
578
|
+
// The chat spine is IMPLIED: thread + input + streaming are always emitted and
|
|
579
|
+
// wired; the construct declares deviations and additions only. Seams below are
|
|
580
|
+
// where later tasks splice capability code; each is a pure string join, so the
|
|
581
|
+
// determinism test keeps holding.
|
|
582
|
+
|
|
583
|
+
function emitApp(c: Construct): string {
|
|
584
|
+
if (c.layout === 'custom') return emitCustomApp(c);
|
|
585
|
+
return `${emitSolidJsImport(c)}import { ChatThread, createKaiChat${emitLayoutImport(c)}${emitCardComponentImport(c)}${emitEmptyComponentImport(c)}${emitHeaderCloseImport(c)} } from '@kitn.ai/ui/solid';
|
|
586
|
+
import type { AttachmentData${emitHistoryTypeImport(c)} } from '@kitn.ai/ui/solid';
|
|
587
|
+
${emitProviderImports(c)}
|
|
588
|
+
${emitCardsImport(c)}
|
|
589
|
+
|
|
590
|
+
${emitProviderSetup(c)}
|
|
591
|
+
${emitHistorySetup(c)}
|
|
592
|
+
|
|
593
|
+
// ChatThread is the kit's own MOST-INTEGRATED chat surface — the same
|
|
594
|
+
// composition <kai-chat>'s facade renders (src/elements/chat.tsx). It owns
|
|
595
|
+
// the message list, the composer (padding, focus ring, the send button) and
|
|
596
|
+
// their layout AS ONE UNIT, so nothing here re-derives spacing, alignment or
|
|
597
|
+
// focus styling by hand: every prior version of this file that hand-composed
|
|
598
|
+
// Thread + PromptInput + Button was restating layout the kit already owns,
|
|
599
|
+
// and every visual defect the owner hit (flush composer, a clipped focus
|
|
600
|
+
// ring) traced back to that restatement. Composing ChatThread directly
|
|
601
|
+
// leaves NOTHING here to restate it with.
|
|
602
|
+
//
|
|
603
|
+
// Capability gating (format rule: an undeclared capability's affordance must
|
|
604
|
+
// be OFF). The construct schema carries ONE capability field so far
|
|
605
|
+
// (capabilities.starters, Task 8) — every other affordance below is gated to
|
|
606
|
+
// "off" unconditionally, not per-construct, until there's a field to gate ON.
|
|
607
|
+
// - webSearch / voice: real ChatThreadProps booleans, default OFF when
|
|
608
|
+
// omitted — set to \`false\` explicitly rather than left implicit, so the
|
|
609
|
+
// gating decision is visible in the emitted source, not just inferred
|
|
610
|
+
// from an absent prop.
|
|
611
|
+
// - suggestions: ChatThread ALREADY owns starter prompts end to end — its
|
|
612
|
+
// own \`suggestions\` prop renders the chips, hides them once
|
|
613
|
+
// \`messages\` is non-empty, and (default \`suggestionMode="submit"\`)
|
|
614
|
+
// calls \`onSubmit\` with the clicked text exactly like a typed submit.
|
|
615
|
+
// So capabilities.starters threads straight into that prop; there is
|
|
616
|
+
// nothing to hand-compose. Omitted (undefined) when no starters are
|
|
617
|
+
// declared, same off-by-default effect as the booleans above.
|
|
618
|
+
// - models: omitted (undefined) — no model switcher; no capabilities field yet.
|
|
619
|
+
// - attachments (the paperclip): gated via ChatThread's \`attach\`/\`accept\`
|
|
620
|
+
// props (kit gap closed — ChatThread forwards both to DefaultPromptInput,
|
|
621
|
+
// mirroring webSearch/voice). ChatThread ALREADY owns the whole
|
|
622
|
+
// round-trip end to end — the paperclip button, staged previews, staging
|
|
623
|
+
// each file as a data URI (never a blob object URL; see
|
|
624
|
+
// AttachmentData.url's doc in components/attachment-types.ts), and
|
|
625
|
+
// handing the staged list back via onSubmit's \`attachments\` — and its
|
|
626
|
+
// Message component ALREADY groups consecutive file parts into one
|
|
627
|
+
// attachment row (message.tsx). So there is nothing to hand-compose
|
|
628
|
+
// here, same lesson as suggestions above: hand-rolling a second picker or
|
|
629
|
+
// a second file-part renderer would restate what ChatThread/Message
|
|
630
|
+
// already own. capabilities.attachments threads straight into
|
|
631
|
+
// attach/accept; the only App.tsx-owned piece is folding the picked
|
|
632
|
+
// attachments into the outgoing message's parts at the submit site
|
|
633
|
+
// (see emitProviderSetup) since createKaiChat's own append/streamAssistant
|
|
634
|
+
// ops don't do that folding themselves.
|
|
635
|
+
// - reasoning: gated via ChatThread's own \`reasoning\` prop (kit gap closed
|
|
636
|
+
// — ChatThread forwards it to every MessageBody as \`reasoningMode\`,
|
|
637
|
+
// mirroring attach/accept). \`'full'\` is both the schema default and
|
|
638
|
+
// ChatThread's own default, so it and an absent field emit no prop at
|
|
639
|
+
// all — the SAME off-by-default convention as every other capability
|
|
640
|
+
// here, just anchored on the medium's existing default instead of an
|
|
641
|
+
// "off" value, since a reasoning disclosure is normal chat behavior, not
|
|
642
|
+
// an opt-in affordance like the paperclip or a starter chip.
|
|
643
|
+
// - empty (the welcome-screen greeting, Task 14): gated via ChatThread's
|
|
644
|
+
// own \`emptyContent\` prop, plain JSX rendered in the SAME shadow tree
|
|
645
|
+
// this file's App already composes ChatThread inside of (see
|
|
646
|
+
// emitEmptyContentProp's own doc for why that boundary needs no Portal
|
|
647
|
+
// at all). \`capabilities.starters\`' chips and the composer still render
|
|
648
|
+
// underneath it: ChatThread's own doc comment on \`emptyContent\` is
|
|
649
|
+
// explicit that it replaces only the empty MESSAGE LIST.
|
|
650
|
+
// - the widget close control (owner feedback on the live demo): a declared
|
|
651
|
+
// \`header.title\` on a \`widget\` layout gets its close button threaded
|
|
652
|
+
// into ChatThread's own header row via \`headerEndContent\`, wired back to
|
|
653
|
+
// Dock's \`controllerRef\` seam through a local closure — see
|
|
654
|
+
// emitDockCloseVar/emitHeaderEndContentProp's docs. No header means no
|
|
655
|
+
// row for it to sit in, so that case is untouched and Dock's own built-in
|
|
656
|
+
// mobile X keeps covering it.
|
|
657
|
+
export function App() {
|
|
658
|
+
${emitDockCloseVar(c, ' ')} return (
|
|
659
|
+
${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)} />
|
|
660
|
+
${emitLayoutClose(c)} );
|
|
661
|
+
}
|
|
662
|
+
`;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* `layout: 'custom'` — the escape hatch's own layout: minimal/no chrome, just
|
|
667
|
+
* the bare chat spine plus the declared `slots` positioned by the consumer.
|
|
668
|
+
* Composed from `Thread` (the message-list primitive, no composer/header/
|
|
669
|
+
* suggestions of its own — components/thread.tsx) + the `PromptInput`
|
|
670
|
+
* compound primitive, NOT `ChatThread`: `ChatThread` bundles its composer
|
|
671
|
+
* INSIDE itself (`DefaultPromptInput`, internal-only), which leaves no seam to
|
|
672
|
+
* splice a slot between the thread and the input the way this layout's
|
|
673
|
+
* placement rule needs. This is the one layout where hand-composing the spine
|
|
674
|
+
* is correct rather than a restatement — every other layout in this file
|
|
675
|
+
* wraps `ChatThread` precisely to avoid this hand-composition (see emitApp's
|
|
676
|
+
* doc comment above).
|
|
677
|
+
*
|
|
678
|
+
* Slot placement is a fixed, deterministic rule, spelled out in the emitted
|
|
679
|
+
* comment: the FIRST declared slot sits above the thread, every other
|
|
680
|
+
* declared slot sits below the composer, in declaration order. There is no
|
|
681
|
+
* vocabulary for a different arrangement (a `position` per slot, an
|
|
682
|
+
* interleaved grain) — reordering means ejecting and rearranging the JSX by
|
|
683
|
+
* hand, which this format's own rule already commits to (no code-in-JSON).
|
|
684
|
+
*
|
|
685
|
+
* Capability gating: only cards are wired here (`Thread` accepts `cardTypes`
|
|
686
|
+
* natively, same as `ChatThread`). starters/attachments/reasoning are NOT
|
|
687
|
+
* wired for `custom` in v1 — `Thread`/`PromptInput` don't carry the kit's own
|
|
688
|
+
* plumbing for those (they live inside `ChatThread`'s composer), and
|
|
689
|
+
* hand-rolling a second copy here is exactly the restatement this file
|
|
690
|
+
* avoids everywhere else. Decided loudly in the emitted comment below, not
|
|
691
|
+
* silently: this is the eject artifact, so a construct author who needs one
|
|
692
|
+
* of them on `custom` adds it directly to the plain Solid file they now own.
|
|
693
|
+
*/
|
|
694
|
+
function emitCustomApp(c: Construct): string {
|
|
695
|
+
const slots = c.slots ?? [];
|
|
696
|
+
const [headerSlot, ...restSlots] = slots;
|
|
697
|
+
const history = c.capabilities?.history;
|
|
698
|
+
const solidJsNames = ['createSignal', ...(history && history.persistence !== 'none' ? ['createEffect'] : [])];
|
|
699
|
+
return `import { ${solidJsNames.join(', ')} } from 'solid-js';
|
|
700
|
+
import { Thread, PromptInput, PromptInputTextarea, PromptInputActions, Button, createKaiChat${emitCardComponentImport(c)} } from '@kitn.ai/ui/solid';
|
|
701
|
+
import type { AttachmentData${emitHistoryTypeImport(c)} } from '@kitn.ai/ui/solid';
|
|
702
|
+
${emitProviderImports(c)}
|
|
703
|
+
${emitCardsImport(c)}
|
|
704
|
+
|
|
705
|
+
${emitProviderSetup(c)}
|
|
706
|
+
${emitHistorySetup(c)}
|
|
707
|
+
|
|
708
|
+
// layout: custom — minimal chrome, no ChatThread/Dock/PaneGroup. The bare
|
|
709
|
+
// spine (Thread + PromptInput) plus the declared slots, positioned by hand so
|
|
710
|
+
// YOU own the surrounding DOM. Capabilities beyond the spine (starters,
|
|
711
|
+
// attachments, reasoning display-mode, reasoningOpen, header.title, empty)
|
|
712
|
+
// are NOT wired here in v1 — this file is the eject artifact; add them the
|
|
713
|
+
// way ChatThread composes them (components/chat-thread.tsx in the kit's own
|
|
714
|
+
// source) if this construct needs them on a custom layout.
|
|
715
|
+
export function App() {
|
|
716
|
+
const [value, setValue] = createSignal('');
|
|
717
|
+
|
|
718
|
+
const handleSubmit = () => {
|
|
719
|
+
const text = value();
|
|
720
|
+
if (!text.trim() || chat.loading()) return;
|
|
721
|
+
setValue('');
|
|
722
|
+
void submit({ value: text, attachments: [] });
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
return (
|
|
726
|
+
<div style={{ height: '100dvh', display: 'flex', 'flex-direction': 'column' }}>
|
|
727
|
+
${emitSlots(headerSlot ? [headerSlot] : undefined, ' ')} <Thread messages={chat.messages()} loading={chat.loading()} class="min-h-0 flex-1"${emitCardTypesProp(c)} />
|
|
728
|
+
{/* Slot placement rule: first declared slot above the thread, every other
|
|
729
|
+
declared slot below the composer, in declaration order. Reorder by
|
|
730
|
+
ejecting — this is the whole grain dimmer. */}
|
|
731
|
+
<PromptInput value={value()} onValueChange={setValue} isLoading={chat.loading()} onSubmit={handleSubmit}>
|
|
732
|
+
<PromptInputTextarea placeholder="Ask anything" />
|
|
733
|
+
<PromptInputActions>
|
|
734
|
+
<Button onClick={handleSubmit}>Send</Button>
|
|
735
|
+
</PromptInputActions>
|
|
736
|
+
</PromptInput>
|
|
737
|
+
${emitSlots(restSlots, ' ')} </div>
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
`;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** header.title -> ChatThread's own \`chatTitle\` prop. Construct-authored/
|
|
744
|
+
* untrusted text like starters/theme.accent/provider.url, so JSON.stringify'd
|
|
745
|
+
* into a real JS string-literal expression. Omitted entirely (not even the
|
|
746
|
+
* prop) when no header is declared — the same off-by-default gating as every
|
|
747
|
+
* capability in this file, even though \`header\` isn't itself a capability. */
|
|
748
|
+
function emitHeaderProp(c: Construct): string {
|
|
749
|
+
const title = c.header?.title;
|
|
750
|
+
if (!title) return '';
|
|
751
|
+
return ` chatTitle={${JSON.stringify(title)}}`;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/** `empty` -> ChatThread's own `emptyContent` prop (chat-thread.tsx), a plain
|
|
755
|
+
* JSX value rendered INSIDE ChatThread's own tree — fully covered by the
|
|
756
|
+
* shadow root's adopted stylesheet, unlike the `empty`/`slot="empty"` boolean
|
|
757
|
+
* pairing this used to go through. That boundary was LIGHT-DOM: `<Portal
|
|
758
|
+
* mount={element}>` manufactured a real child of the host element tagged
|
|
759
|
+
* `slot="empty"` so ChatThread's `<slot name="empty">` could redistribute it
|
|
760
|
+
* — a detour needed only because a shadow `<slot>` redistributes light-DOM
|
|
761
|
+
* children of the HOST, never a Solid sibling's own JSX. It worked, but light
|
|
762
|
+
* DOM sits outside the shadow root's adopted stylesheet, so the Tailwind
|
|
763
|
+
* utility classes the kit's own `Empty` composition is built from resolved to
|
|
764
|
+
* nothing — the greeting rendered, unstyled (owner report against the live
|
|
765
|
+
* widget: "doesn't look like the empty component"). `App` already composes
|
|
766
|
+
* `ChatThread` directly as a plain Solid component in the SAME shadow tree
|
|
767
|
+
* `defineWebComponent` attaches, so there is no boundary to cross here at
|
|
768
|
+
* all — `emptyContent` just hands the JSX straight down, and it inherits the
|
|
769
|
+
* same styling as the rest of `App`. No Portal, no host-element param on
|
|
770
|
+
* `App`, no light-DOM indirection.
|
|
771
|
+
*
|
|
772
|
+
* `capabilities.starters`' chips and the composer still render underneath
|
|
773
|
+
* it: ChatThread's own doc comment on `empty`/`emptyContent` is explicit
|
|
774
|
+
* that this REPLACES only the empty MESSAGE LIST.
|
|
775
|
+
*
|
|
776
|
+
* Uses the kit's own `Empty`/`EmptyHeader`/`EmptyMedia`/`EmptyTitle`/
|
|
777
|
+
* `EmptyDescription` composition (components/empty.tsx) rather than hand-
|
|
778
|
+
* rolled markup — the same "don't restate the kit's own layout" rule
|
|
779
|
+
* `emitApp`'s header comment states for ChatThread itself. `title`/
|
|
780
|
+
* `description` are construct-authored/untrusted text, JSON.stringify'd into
|
|
781
|
+
* real JS string-literal expressions like every other free-text field in
|
|
782
|
+
* this file; `icon` is schema-validated by `isSafeUrl` (schema.ts) before
|
|
783
|
+
* codegen ever sees it, the same policy `widget.launcherIcon` uses. */
|
|
784
|
+
function emitEmptyContentProp(c: Construct): string {
|
|
785
|
+
const empty = c.empty;
|
|
786
|
+
if (!empty) return '';
|
|
787
|
+
const title = `<EmptyTitle>{${JSON.stringify(empty.title)}}</EmptyTitle>`;
|
|
788
|
+
const icon = empty.icon
|
|
789
|
+
? `<EmptyMedia><img src={${JSON.stringify(empty.icon)}} alt="" style={{ width: '40px', height: '40px', 'border-radius': '9999px' }} /></EmptyMedia>`
|
|
790
|
+
: '';
|
|
791
|
+
const description = empty.description
|
|
792
|
+
? `<EmptyDescription>{${JSON.stringify(empty.description)}}</EmptyDescription>`
|
|
793
|
+
: '';
|
|
794
|
+
return ` emptyContent={<Empty><EmptyHeader>${icon}${title}${description}</EmptyHeader></Empty>}`;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/** The `Empty` composition components `emitEmptyContentProp` needs, appended
|
|
798
|
+
* onto the same `@kitn.ai/ui/solid` import ChatThread/createKaiChat already
|
|
799
|
+
* use — never a second import statement for the same module. `EmptyMedia`/
|
|
800
|
+
* `EmptyDescription` are named only when `icon`/`description` are actually
|
|
801
|
+
* declared: `verify:scaffold` compiles emitted output with `tsc --strict
|
|
802
|
+
* --noUnusedLocals`, so an always-imported-but-sometimes-unused name would
|
|
803
|
+
* fail that gate the moment a construct omits one. */
|
|
804
|
+
function emitEmptyComponentImport(c: Construct): string {
|
|
805
|
+
if (!c.empty) return '';
|
|
806
|
+
let names = ', Empty, EmptyHeader, EmptyTitle';
|
|
807
|
+
if (c.empty.icon) names += ', EmptyMedia';
|
|
808
|
+
if (c.empty.description) names += ', EmptyDescription';
|
|
809
|
+
return names;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/** `widget` layout with a declared `header.title` gets its own close control
|
|
813
|
+
* integrated INTO ChatThread's header row instead of relying solely on
|
|
814
|
+
* Dock's own floating mobile X (see `dock.tsx`'s `hideClose` doc for the
|
|
815
|
+
* "why": a header-row X and a floating X over that same row read as
|
|
816
|
+
* unintentional together — owner feedback against the live widget). No
|
|
817
|
+
* header means no row for a close control to sit in at all, so this stays
|
|
818
|
+
* false and Dock's built-in fallback X keeps covering that case unchanged. */
|
|
819
|
+
function widgetHasHeaderClose(c: Construct): boolean {
|
|
820
|
+
return c.layout === 'widget' && !!c.header?.title;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/** The local closure `emitHeaderEndContentProp`/`emitDockControllerRef` share:
|
|
824
|
+
* Dock's `controllerRef` hands back `{ open, setOpen }` (ui/dock.tsx) — the
|
|
825
|
+
* existing imperative seam, not a new one — and this captures `setOpen`
|
|
826
|
+
* behind a plain function so ChatThread's `headerEndContent` button (which
|
|
827
|
+
* renders as a sibling, not a Dock descendant) can call it. Declared inside
|
|
828
|
+
* `App()`, not at module scope: `App()` runs once per widget instance, and a
|
|
829
|
+
* module-level variable would let one instance's close button reach into
|
|
830
|
+
* another's Dock if two ever rendered on the same page. */
|
|
831
|
+
function emitDockCloseVar(c: Construct, indent: string): string {
|
|
832
|
+
return widgetHasHeaderClose(c) ? `${indent}let dockClose: (() => void) | undefined;\n` : '';
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/** Threads `emitDockCloseVar`'s closure onto `<Dock controllerRef>`. */
|
|
836
|
+
function emitDockControllerRef(c: Construct): string {
|
|
837
|
+
return widgetHasHeaderClose(c) ? ' controllerRef={(api) => (dockClose = () => api.setOpen(false))}' : '';
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** Suppresses Dock's own built-in mobile close X (see its `hideClose` doc)
|
|
841
|
+
* when ChatThread's header row is carrying an equivalent control instead —
|
|
842
|
+
* otherwise the two stack, one floating over the other's row. */
|
|
843
|
+
function emitDockHideClose(c: Construct): string {
|
|
844
|
+
return widgetHasHeaderClose(c) ? ' hideClose={true}' : '';
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** ChatThread's `headerEndContent` prop (chat-thread.tsx): the close button
|
|
848
|
+
* itself, sharing the header row with the title instead of floating as a
|
|
849
|
+
* second, visually unrelated control — the owner's stated preference over
|
|
850
|
+
* Dock's previous "reserved dead-row band" fix. Reuses the kit's own
|
|
851
|
+
* `DockCloseGlyph` (the same X Dock's built-in button draws) and `Button`
|
|
852
|
+
* (ghost/icon-sm, the same weight ChatThread's own header controls use —
|
|
853
|
+
* see `ModelSwitcher`'s trigger) rather than hand-rolling either, and calls
|
|
854
|
+
* back into `emitDockCloseVar`'s closure to actually close the panel —
|
|
855
|
+
* Dock's own `controllerRef` seam, not a new one. */
|
|
856
|
+
function emitHeaderEndContentProp(c: Construct): string {
|
|
857
|
+
if (!widgetHasHeaderClose(c)) return '';
|
|
858
|
+
return ` headerEndContent={<Button variant="ghost" size="icon-sm" aria-label="Close ${c.name}" onClick={() => dockClose?.()}><DockCloseGlyph /></Button>}`;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/** `Button`/`DockCloseGlyph`, needed only by `emitHeaderEndContentProp` above
|
|
862
|
+
* — appended onto the same `@kitn.ai/ui/solid` import as `emitEmptyComponentImport`,
|
|
863
|
+
* never a second import statement for the module. */
|
|
864
|
+
function emitHeaderCloseImport(c: Construct): string {
|
|
865
|
+
return widgetHasHeaderClose(c) ? ', Button, DockCloseGlyph' : '';
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/** capabilities.attachments -> ChatThread's own \`attach\`/\`accept\` props.
|
|
869
|
+
* Undeclared keeps the explicit off-by-default gating (\`attach={false}\`,
|
|
870
|
+
* matching webSearch/voice above). Declared flips \`attach={true}\` and
|
|
871
|
+
* threads the accept list through — construct-authored/untrusted like
|
|
872
|
+
* \`starters\`/\`theme.accent\`/\`provider.url\`, so JSON.stringify'd into a
|
|
873
|
+
* real JS string-literal expression rather than a raw JSX attribute
|
|
874
|
+
* string (JSX attribute strings don't interpret escapes the way JS string
|
|
875
|
+
* literals do, so a raw \`accept="..."\` would be a breakout surface for a
|
|
876
|
+
* hostile media-type entry containing a \`"\`). */
|
|
877
|
+
function emitAttachProps(c: Construct): string {
|
|
878
|
+
const attachments = c.capabilities?.attachments;
|
|
879
|
+
if (!attachments) return ' attach={false}';
|
|
880
|
+
return ` attach={true} accept={${JSON.stringify(attachments.accept.join(','))}}`;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/** capabilities.starters -> ChatThread's own \`suggestions\` prop. Starter
|
|
884
|
+
* strings are construct-authored (untrusted the same way theme.accent and
|
|
885
|
+
* provider.url are) — JSON.stringify produces a real JS array-of-string-
|
|
886
|
+
* literals expression, the same safe-interpolation convention used for the
|
|
887
|
+
* accent (element.tsx) and the endpoint url (fetch() above): no quote,
|
|
888
|
+
* backslash or line-separator payload can break out of it. Omitted
|
|
889
|
+
* entirely (not even the prop) when no starters are declared, matching the
|
|
890
|
+
* off-by-default gating for every other capability. */
|
|
891
|
+
function emitStartersProp(c: Construct): string {
|
|
892
|
+
const starters = c.capabilities?.starters;
|
|
893
|
+
if (!starters || starters.length === 0) return '';
|
|
894
|
+
return ` suggestions={${JSON.stringify(starters)}}`;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/** capabilities.reasoning -> ChatThread's own `reasoning` prop. `'full'`
|
|
898
|
+
* and absent are the SAME thing (the schema default, matching ChatThread's
|
|
899
|
+
* own default) so both emit nothing at all — the off-by-default gating
|
|
900
|
+
* convention every other capability in this file follows: only a value that
|
|
901
|
+
* DEVIATES from the medium's default costs a byte in the emitted source.
|
|
902
|
+
* `'compact'`/`'off'` are plain string literals, not JSON.stringify'd like
|
|
903
|
+
* starters/accept/url — the schema already constrains this to one of three
|
|
904
|
+
* fixed enum members (schema.ts), so unlike those fields there is no
|
|
905
|
+
* construct-authored free text here to escape. */
|
|
906
|
+
function emitReasoningProp(c: Construct): string {
|
|
907
|
+
const reasoning = c.capabilities?.reasoning;
|
|
908
|
+
if (!reasoning || reasoning === 'full') return '';
|
|
909
|
+
return ` reasoning="${reasoning}"`;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/** capabilities.reasoningOpen -> ChatThread's own `reasoningOpen` prop. Only
|
|
913
|
+
* `true` costs a byte (off-by-default, matching every capability here);
|
|
914
|
+
* false/absent matches the kit's own new default (closed chip). */
|
|
915
|
+
function emitReasoningOpenProp(c: Construct): string {
|
|
916
|
+
return c.capabilities?.reasoningOpen === true ? ' reasoningOpen={true}' : '';
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/** capabilities.history -> whether the App module needs `createEffect`
|
|
920
|
+
* (both persisted variants react to `chat.messages()` changing; `none`/
|
|
921
|
+
* absent needs no extra Solid import at all, matching the off-by-default
|
|
922
|
+
* gating everywhere else in this file). */
|
|
923
|
+
function emitSolidJsImport(c: Construct): string {
|
|
924
|
+
const history = c.capabilities?.history;
|
|
925
|
+
if (!history || history.persistence === 'none') return '';
|
|
926
|
+
return `import { createEffect } from 'solid-js';\n`;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/** capabilities.history -> whether the AttachmentData type import also needs
|
|
930
|
+
* ChatMessage (only the persisted variants read/write full ChatMessage[]
|
|
931
|
+
* arrays). */
|
|
932
|
+
function emitHistoryTypeImport(c: Construct): string {
|
|
933
|
+
const history = c.capabilities?.history;
|
|
934
|
+
if (!history || history.persistence === 'none') return '';
|
|
935
|
+
// The enclosing statement is already `import type { ... }` (AttachmentData),
|
|
936
|
+
// so this must NOT repeat the `type` modifier inside the braces — `import
|
|
937
|
+
// type { AttachmentData, type ChatMessage }` is a TS syntax error.
|
|
938
|
+
return ', ChatMessage';
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/** capabilities.history -> the persistence block spliced after
|
|
942
|
+
* createKaiChat/submit (emitProviderSetup). `none`/absent emits nothing at
|
|
943
|
+
* all — the format rule (undeclared capability's affordance is OFF).
|
|
944
|
+
*
|
|
945
|
+
* `local`: keyed by the construct's own tag (one thread per construct, no
|
|
946
|
+
* cross-construct collision) — restoring on mount MUST hand createKaiChat's
|
|
947
|
+
* setMessages a NEW array reference (the kit's reactivity contract; see
|
|
948
|
+
* CLAUDE.md), which the updater-returns-parsed-array form does for free. A
|
|
949
|
+
* parsed value that is well-formed JSON but the WRONG SHAPE (an object, a
|
|
950
|
+
* number, ...) is just as dangerous as a storage exception — handing it to
|
|
951
|
+
* `chat.setMessages` would crash ChatThread's render — so it gets the same
|
|
952
|
+
* `Array.isArray` gate as the endpoint variant below, not just a try/catch
|
|
953
|
+
* around the parse.
|
|
954
|
+
* localStorage access is wrapped: it can throw in private mode or over
|
|
955
|
+
* quota, and a corrupt/foreign value under the key must not white-screen —
|
|
956
|
+
* neither failure is guessed at silently, both fall back to running
|
|
957
|
+
* in-memory (decide loudly: see the comment emitted alongside).
|
|
958
|
+
* Retention/eviction (how much, how long) is deliberately absent — an
|
|
959
|
+
* application-layer decision (component-scope-boundary), not this
|
|
960
|
+
* construct's to make.
|
|
961
|
+
*
|
|
962
|
+
* `endpoint`: the CONSUMER's own thread route — GET on mount (kit parses
|
|
963
|
+
* the response as ChatMessage[]; a non-OK response, a rejected fetch, or a
|
|
964
|
+
* non-array body all fall back to an empty thread rather than throwing or
|
|
965
|
+
* crashing render — matching the shape-check discipline above), PUT on
|
|
966
|
+
* every change. Both fetches are wrapped (try/catch around the GET chain,
|
|
967
|
+
* `.catch` on the PUT) and decide loudly on failure (`console.error`) —
|
|
968
|
+
* mirroring the adjacent provider-endpoint fetch's own try/catch +
|
|
969
|
+
* `stream.abort` pattern, not a silent swallow. The `hydrated` flag guards
|
|
970
|
+
* against the mount-load's own setMessages call immediately re-triggering a
|
|
971
|
+
* PUT that writes back exactly what was just read — but a FAILED GET must
|
|
972
|
+
* still flip it, in `finally`: a transient GET failure (offline/CORS/DNS)
|
|
973
|
+
* degrading to "start fresh, keep saving" is the recoverable failure mode;
|
|
974
|
+
* leaving `hydrated` false forever would permanently disable every future
|
|
975
|
+
* PUT for the tab's life over one blip. No retry/backoff — that belongs to
|
|
976
|
+
* the app, not this construct (component-scope-boundary). url is
|
|
977
|
+
* construct-authored/untrusted like theme.accent and provider.url, so it is
|
|
978
|
+
* JSON.stringify'd at both fetch call sites — never string-concatenated
|
|
979
|
+
* (see the endpoint-provider comment on this same class of bug). */
|
|
980
|
+
/** Top-level userId -> the `x-kai-user-id` header on every emitted fetch that
|
|
981
|
+
* talks to the consumer's own backend (the endpoint provider's chat POST, and
|
|
982
|
+
* history's endpoint GET/PUT) — so a route can tell which user's thread this
|
|
983
|
+
* is. `local` persistence folds it into THREAD_KEY instead (see
|
|
984
|
+
* emitHistorySetup) — no network call to header there. userId is
|
|
985
|
+
* construct-authored data (like theme.accent/provider.url), so it is
|
|
986
|
+
* JSON.stringify'd wherever it is interpolated — never string-concatenated. */
|
|
987
|
+
function emitUserIdHeaderEntry(c: Construct): string {
|
|
988
|
+
return c.userId ? `, 'x-kai-user-id': ${JSON.stringify(c.userId)}` : '';
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function emitHistorySetup(c: Construct): string {
|
|
992
|
+
const history = c.capabilities?.history;
|
|
993
|
+
if (!history || history.persistence === 'none') return '';
|
|
994
|
+
|
|
995
|
+
if (history.persistence === 'local') {
|
|
996
|
+
const key = JSON.stringify(c.userId ? `kai:${c.name}:${c.userId}:thread` : `kai:${c.name}:thread`);
|
|
997
|
+
return `
|
|
998
|
+
// History: persisted locally in this browser, keyed by the element tag. What to
|
|
999
|
+
// retain and for how long is an app decision — clear the key to reset.
|
|
1000
|
+
const THREAD_KEY = ${key};
|
|
1001
|
+
try {
|
|
1002
|
+
const saved = localStorage.getItem(THREAD_KEY);
|
|
1003
|
+
if (saved) {
|
|
1004
|
+
const parsed: unknown = JSON.parse(saved);
|
|
1005
|
+
if (Array.isArray(parsed)) {
|
|
1006
|
+
chat.setMessages(() => parsed as ChatMessage[]);
|
|
1007
|
+
} else {
|
|
1008
|
+
console.warn(\`[\${THREAD_KEY}] stored history was not an array; ignoring and starting fresh\`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
} catch { /* storage unavailable or corrupt: run in-memory */ }
|
|
1012
|
+
createEffect(() => {
|
|
1013
|
+
try {
|
|
1014
|
+
localStorage.setItem(THREAD_KEY, JSON.stringify(chat.messages()));
|
|
1015
|
+
} catch { /* storage unavailable: run in-memory */ }
|
|
1016
|
+
});
|
|
1017
|
+
`;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const url = JSON.stringify(history.url);
|
|
1021
|
+
return `
|
|
1022
|
+
// History: persisted to your endpoint (GET on mount, PUT on every change) —
|
|
1023
|
+
// the kit PARSES, this app FETCHES; your route owns the storage and what to
|
|
1024
|
+
// retain and for how long. \`hydrated\` guards the mount-load from immediately
|
|
1025
|
+
// PUTting back what it just loaded, but still flips on a FAILED load — one
|
|
1026
|
+
// offline/CORS/DNS blip degrades to "start fresh, keep saving", not
|
|
1027
|
+
// "never save again".
|
|
1028
|
+
let hydrated = false;
|
|
1029
|
+
(async () => {
|
|
1030
|
+
try {
|
|
1031
|
+
const r = await fetch(${url}${c.userId ? `, { headers: { 'x-kai-user-id': ${JSON.stringify(c.userId)} } }` : ''});
|
|
1032
|
+
const saved: unknown = r.ok ? await r.json() : [];
|
|
1033
|
+
if (Array.isArray(saved)) {
|
|
1034
|
+
chat.setMessages(() => saved as ChatMessage[]);
|
|
1035
|
+
} else {
|
|
1036
|
+
console.warn('history endpoint returned a non-array body; ignoring and starting fresh');
|
|
1037
|
+
}
|
|
1038
|
+
} catch (err) {
|
|
1039
|
+
console.error('history endpoint GET failed; starting fresh (will keep saving)', err);
|
|
1040
|
+
} finally {
|
|
1041
|
+
hydrated = true;
|
|
1042
|
+
}
|
|
1043
|
+
})();
|
|
1044
|
+
createEffect(() => {
|
|
1045
|
+
const snapshot = chat.messages();
|
|
1046
|
+
if (!hydrated) return;
|
|
1047
|
+
fetch(${url}, {
|
|
1048
|
+
method: 'PUT',
|
|
1049
|
+
headers: { 'content-type': 'application/json'${emitUserIdHeaderEntry(c)} },
|
|
1050
|
+
body: JSON.stringify(snapshot),
|
|
1051
|
+
}).catch((err) => {
|
|
1052
|
+
console.error('history endpoint PUT failed; this change was not persisted', err);
|
|
1053
|
+
});
|
|
1054
|
+
});
|
|
1055
|
+
`;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function emitProviderImports(c: Construct): string {
|
|
1059
|
+
if (c.provider.mode === 'mock') {
|
|
1060
|
+
return `import { createMockResponder } from '@kitn.ai/ui/state';
|
|
1061
|
+
import { readOpenAIStream } from '@kitn.ai/ui/wire';`;
|
|
1062
|
+
}
|
|
1063
|
+
const read = c.provider.wire === 'openai' ? 'readOpenAIStream' : 'readAnthropicStream';
|
|
1064
|
+
const encode = c.provider.wire === 'openai' ? 'toOpenAIMessages' : 'toAnthropicMessages';
|
|
1065
|
+
return `import { ${read}, ${encode} } from '@kitn.ai/ui/wire';`;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function emitProviderSetup(c: Construct): string {
|
|
1069
|
+
// ChatThread owns its own composer draft (uncontrolled — no `value` prop
|
|
1070
|
+
// passed below) and clears it after submit itself; `onSubmit` hands back
|
|
1071
|
+
// the value directly, so there's no PromptInput-specific signal-reading
|
|
1072
|
+
// workaround to carry here any more.
|
|
1073
|
+
if (c.provider.mode === 'mock') {
|
|
1074
|
+
const cardsNote = c.cards
|
|
1075
|
+
? `
|
|
1076
|
+
// Cards demo keylessly: createMockResponder() can already SCRIPT a tool call
|
|
1077
|
+
// (\`replies: [{ toolCalls: [...] }]\`, F-35), so a scripted turn calling
|
|
1078
|
+
// \`kai_<card name>\` renders exactly like a live model's would, below.`
|
|
1079
|
+
: '';
|
|
1080
|
+
return `// Provider seam: mock — keyless, streams locally, announces itself once.
|
|
1081
|
+
// Swap for provider.mode "endpoint" in the construct and re-run kai dev; the
|
|
1082
|
+
// generated fetch keeps this exact shape (the seam is the point).${cardsNote}
|
|
1083
|
+
const respond = createMockResponder();
|
|
1084
|
+
const chat = createKaiChat();
|
|
1085
|
+
|
|
1086
|
+
async function submit(detail: { value: string; attachments: AttachmentData[] }) {
|
|
1087
|
+
if (!detail.value.trim() || chat.loading()) return;
|
|
1088
|
+
chat.append({
|
|
1089
|
+
id: crypto.randomUUID(),
|
|
1090
|
+
role: 'user',
|
|
1091
|
+
parts: [
|
|
1092
|
+
{ type: 'text', text: detail.value },
|
|
1093
|
+
...detail.attachments.map((attachment) => ({ type: 'file' as const, attachment })),
|
|
1094
|
+
],
|
|
1095
|
+
});
|
|
1096
|
+
const stream = chat.streamAssistant();
|
|
1097
|
+
try {
|
|
1098
|
+
await readOpenAIStream(respond(detail.value), stream);${emitApplyCardTools(c)}
|
|
1099
|
+
stream.done();
|
|
1100
|
+
} catch (err) {
|
|
1101
|
+
stream.abort(err instanceof Error ? err.message : String(err));
|
|
1102
|
+
}
|
|
1103
|
+
}`;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const { url, wire } = c.provider;
|
|
1107
|
+
const read = wire === 'openai' ? 'readOpenAIStream' : 'readAnthropicStream';
|
|
1108
|
+
const encode = wire === 'openai' ? 'toOpenAIMessages' : 'toAnthropicMessages';
|
|
1109
|
+
// provider.url is an UNCONSTRAINED z.string() (schema.ts). It is NEVER
|
|
1110
|
+
// embedded in this comment — a `//` line comment is ended by a raw
|
|
1111
|
+
// U+2028/U+2029 line separator (a valid JS line terminator that
|
|
1112
|
+
// commentSafe's \r\n strip does not catch), so a url containing one of
|
|
1113
|
+
// those code points could close the comment early and let the rest of the
|
|
1114
|
+
// url execute as JS. commentSafe is a comment-escaping tool and this is a
|
|
1115
|
+
// hand-rolled-escaping trap for untrusted input by construction, so the
|
|
1116
|
+
// fix is to never hand-roll it here: the url appears ONLY on the fetch()
|
|
1117
|
+
// line below, via JSON.stringify, which is a real JS string literal (not a
|
|
1118
|
+
// comment) and immune to this class of bug.
|
|
1119
|
+
return `// Provider seam: YOUR endpoint (${wire} wire, see the fetch call below
|
|
1120
|
+
// for the URL). The kit PARSES, this component FETCHES — no key, no
|
|
1121
|
+
// provider SDK, no client in here. Your route holds the key and re-frames to
|
|
1122
|
+
// the provider; the kai MCP scaffold tool emits one for your framework.
|
|
1123
|
+
const chat = createKaiChat();
|
|
1124
|
+
|
|
1125
|
+
async function submit(detail: { value: string; attachments: AttachmentData[] }) {
|
|
1126
|
+
if (!detail.value.trim() || chat.loading()) return;
|
|
1127
|
+
chat.append({
|
|
1128
|
+
id: crypto.randomUUID(),
|
|
1129
|
+
role: 'user',
|
|
1130
|
+
parts: [
|
|
1131
|
+
{ type: 'text', text: detail.value },
|
|
1132
|
+
...detail.attachments.map((attachment) => ({ type: 'file' as const, attachment })),
|
|
1133
|
+
],
|
|
1134
|
+
});
|
|
1135
|
+
const stream = chat.streamAssistant();
|
|
1136
|
+
try {
|
|
1137
|
+
const response = await fetch(${JSON.stringify(url)}, {
|
|
1138
|
+
method: 'POST',
|
|
1139
|
+
headers: { 'content-type': 'application/json'${emitUserIdHeaderEntry(c)} },
|
|
1140
|
+
body: JSON.stringify({ messages: ${encode}(chat.messages())${emitToolsField(c)} }),
|
|
1141
|
+
});
|
|
1142
|
+
if (!response.ok) throw new Error(\`endpoint responded \${response.status}\`);
|
|
1143
|
+
await ${read}(response, stream);${emitApplyCardTools(c)}
|
|
1144
|
+
stream.done();
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
stream.abort(err instanceof Error ? err.message : String(err));
|
|
1147
|
+
}
|
|
1148
|
+
}`;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/** The layout-conditional named import spliced onto the `@kitn.ai/ui/solid`
|
|
1152
|
+
* import list in App.tsx: `Dock` only for `widget`, `PaneGroup` only for
|
|
1153
|
+
* `split` — nothing for `fullscreen`/`aside`, which are plain styled
|
|
1154
|
+
* containers (the kit has no dedicated fullscreen/docked-aside component;
|
|
1155
|
+
* see the emitLayoutOpen doc for why that's the honest choice here rather
|
|
1156
|
+
* than a hand-rolled component of our own). Gated so the generated
|
|
1157
|
+
* project's own `noUnusedLocals` never trips on an import used by a layout
|
|
1158
|
+
* that isn't this construct's. */
|
|
1159
|
+
function emitLayoutImport(c: Construct): string {
|
|
1160
|
+
switch (c.layout) {
|
|
1161
|
+
case 'widget':
|
|
1162
|
+
return ', Dock';
|
|
1163
|
+
case 'split':
|
|
1164
|
+
return ', WorkspaceShell';
|
|
1165
|
+
case 'fullscreen':
|
|
1166
|
+
case 'aside':
|
|
1167
|
+
return '';
|
|
1168
|
+
case 'custom':
|
|
1169
|
+
// Unreachable: emitApp special-cases 'custom' into emitCustomApp before
|
|
1170
|
+
// this is ever called (custom's spine is Thread + PromptInput, not
|
|
1171
|
+
// ChatThread, so there is no shared import line to splice onto). Kept
|
|
1172
|
+
// only so this switch stays exhaustive over the widened layout enum.
|
|
1173
|
+
return '';
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/** Declared `slots`, one `<slot name="...">` per entry in declaration order.
|
|
1178
|
+
* Slot names are already schema-validated to `^[a-z][a-z0-9-]*$` (schema.ts)
|
|
1179
|
+
* — a closed character set with no quote/backslash, so they're interpolated
|
|
1180
|
+
* directly into the attribute rather than JSON.stringify'd like the
|
|
1181
|
+
* free-text construct-authored fields (starters, theme.accent, provider.url)
|
|
1182
|
+
* elsewhere in this file. `indent` matches the surrounding JSX depth. */
|
|
1183
|
+
function emitSlots(slots: readonly string[] | undefined, indent: string): string {
|
|
1184
|
+
if (!slots || slots.length === 0) return '';
|
|
1185
|
+
return slots.map((name) => `${indent}<slot name="${name}" />\n`).join('');
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* The layout shell wrapping ChatThread — one pair (open above, close below)
|
|
1190
|
+
* per `layout`, composing the kit's own layout primitives over a hand-rolled
|
|
1191
|
+
* div wherever the kit ships one:
|
|
1192
|
+
*
|
|
1193
|
+
* - `widget`: the kit's Dock (launcher + panel + focus contract) — unchanged
|
|
1194
|
+
* from before Task 12. No theming wrapper needed here — the accent lands
|
|
1195
|
+
* on the HOST element from element.tsx's facade (see emitElement), which
|
|
1196
|
+
* reaches both the launcher (a DOM sibling of this panel content, outside
|
|
1197
|
+
* Dock's own `children`) and everything below via normal custom-property
|
|
1198
|
+
* inheritance from :host down through the whole shadow tree.
|
|
1199
|
+
* - `fullscreen`: the kit has no dedicated "fill the viewport" component —
|
|
1200
|
+
* this genuinely is just sizing, not chrome — so a minimally-styled
|
|
1201
|
+
* `<div>` (100dvh, column flex) IS the honest composition, not a
|
|
1202
|
+
* restatement of something the kit already owns.
|
|
1203
|
+
* - `aside`: same reasoning — a persistent, single-edge docked panel is a
|
|
1204
|
+
* styled container (fixed inline-end column, the kit's own
|
|
1205
|
+
* `--kai-color-border` token for the divider), not a kit component. `dvh`/
|
|
1206
|
+
* logical properties (`inset-inline-end`, `border-inline-start`) keep it
|
|
1207
|
+
* correct under RTL and mobile viewport chrome the same way `100dvh` does
|
|
1208
|
+
* for fullscreen.
|
|
1209
|
+
* - `split`: composes the kit's real `WorkspaceShell` (components/
|
|
1210
|
+
* workspace-shell.tsx) for its frame -- chat in `children` (the main
|
|
1211
|
+
* region), the end pane's `<slot name="pane">` seam projected via `end`.
|
|
1212
|
+
* Superseded from Task 12's `PaneGroup` (recorded decision 2): `PaneGroup`
|
|
1213
|
+
* is an editor GROUP contract (src/ui/pane-group.tsx) -- a tab strip over
|
|
1214
|
+
* ONE content area -- so getting two SIMULTANEOUS panes out of it meant a
|
|
1215
|
+
* single always-active tab whose one body was a hand-rolled flex row doing
|
|
1216
|
+
* the actual two-column math; the "kit component" was supplying a frame
|
|
1217
|
+
* the composition didn't use for its defining feature (a resizable split)
|
|
1218
|
+
* at all. `WorkspaceShell` already IS a two-region layout with a REAL
|
|
1219
|
+
* draggable splitter between them (it composes `ResizablePanelGroup`
|
|
1220
|
+
* internally) -- start/end aside width props and collapse are left at
|
|
1221
|
+
* their defaults here (this construct wants exactly one fixed end pane,
|
|
1222
|
+
* not a full workspace chrome); `drawerBelow={480}` IS wired (Task 19d) so
|
|
1223
|
+
* split gets the kit's own mobile takeover at the same breakpoint every
|
|
1224
|
+
* other layout uses, so the split's math is the kit's
|
|
1225
|
+
* own, not a restatement.
|
|
1226
|
+
*
|
|
1227
|
+
* `custom` is handled entirely by `emitCustomApp` instead (its spine is
|
|
1228
|
+
* Thread + PromptInput composed by hand, not ChatThread, so there's no shared
|
|
1229
|
+
* chrome to open/close here) — `emitApp` special-cases it before either of
|
|
1230
|
+
* these is called. Both switches still carry a `case 'custom'` so they stay
|
|
1231
|
+
* exhaustive over the widened layout enum: TypeScript is what caught this
|
|
1232
|
+
* exact gap when Task 13 added the enum member, and a `default` would have
|
|
1233
|
+
* hidden it again for the next one.
|
|
1234
|
+
*/
|
|
1235
|
+
/** widget.position -> Dock's own `position` prop. A closed DockPosition enum
|
|
1236
|
+
* (schema-constrained), so plain string interpolation is safe — no
|
|
1237
|
+
* construct-authored free text here, unlike launcherIcon below. */
|
|
1238
|
+
function emitDockPosition(c: Construct): string {
|
|
1239
|
+
const w = c.layout === 'widget' ? c.widget : undefined;
|
|
1240
|
+
return w?.position ? ` position="${w.position}"` : '';
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/** widget.launcherIcon -> Dock's `launcher` prop, replacing the built-in
|
|
1244
|
+
* closed-state glyph with an <img>. launcherIcon is construct-authored/
|
|
1245
|
+
* untrusted text (like theme.accent/provider.url elsewhere in this file),
|
|
1246
|
+
* so it is JSON.stringify'd into a real JS string-literal expression, never
|
|
1247
|
+
* interpolated into a raw JSX attribute string. */
|
|
1248
|
+
function emitDockLauncher(c: Construct): string {
|
|
1249
|
+
const w = c.layout === 'widget' ? c.widget : undefined;
|
|
1250
|
+
if (!w?.launcherIcon) return '';
|
|
1251
|
+
return ` launcher={<img src={${JSON.stringify(w.launcherIcon)}} alt="" style={{ width: '24px', height: '24px', 'border-radius': '9999px' }} />}`;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/** widget.defaultOpen -> Dock's own `defaultOpen` prop. Only `true` costs a
|
|
1255
|
+
* byte — `false`/absent matches Dock's own default (closed), same
|
|
1256
|
+
* off-by-default convention as every other capability in this file. */
|
|
1257
|
+
function emitDockDefaultOpen(c: Construct): string {
|
|
1258
|
+
const w = c.layout === 'widget' ? c.widget : undefined;
|
|
1259
|
+
return w?.defaultOpen === true ? ' defaultOpen={true}' : '';
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function emitLayoutOpen(c: Construct): string {
|
|
1263
|
+
switch (c.layout) {
|
|
1264
|
+
case 'widget':
|
|
1265
|
+
return ` <Dock label="${c.name}"${emitDockPosition(c)}${emitDockLauncher(c)}${emitDockDefaultOpen(c)}${emitDockHideClose(c)}${emitDockControllerRef(c)}>\n`;
|
|
1266
|
+
case 'fullscreen':
|
|
1267
|
+
return ` <div style={{ height: '100dvh', display: 'flex', 'flex-direction': 'column' }}>\n`;
|
|
1268
|
+
case 'aside':
|
|
1269
|
+
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)' }}>
|
|
1270
|
+
{/* Mirrors Dock's own narrow-viewport full-bleed rule (ui/dock.tsx:229-240)
|
|
1271
|
+
— aside has no dedicated kit component (see the emitLayoutOpen doc
|
|
1272
|
+
comment above), so this is the honest hand-rolled equivalent, not a
|
|
1273
|
+
new responsive strategy. */}
|
|
1274
|
+
<style>{\`@media (max-width: 480px) { [data-kai-layout="aside"] { inset: 0; width: auto; height: auto; border-inline-start: 0; } }\`}</style>
|
|
1275
|
+
`;
|
|
1276
|
+
case 'split':
|
|
1277
|
+
// drawerBelow: split's mobile takeover is the kit's OWN WorkspaceShell
|
|
1278
|
+
// capability (components/workspace-shell.tsx), not hand-rolled CSS — wiring
|
|
1279
|
+
// it here is composition-over-reauthoring, not a media-query duplicate. 480
|
|
1280
|
+
// matches Dock's own breakpoint (ui/dock.tsx:229) so every layout takes over
|
|
1281
|
+
// at the same viewport width.
|
|
1282
|
+
return ` <div style={{ height: '100dvh' }}>\n <WorkspaceShell class="h-full" drawerBelow={480} end={\n <div style={{ height: '100%', overflow: 'auto' }}>\n <slot name="pane" />\n </div>\n }>\n`;
|
|
1283
|
+
case 'custom':
|
|
1284
|
+
return ''; // unreachable — see the block comment above
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function emitLayoutClose(c: Construct): string {
|
|
1289
|
+
switch (c.layout) {
|
|
1290
|
+
case 'widget':
|
|
1291
|
+
return ` </Dock>\n`;
|
|
1292
|
+
case 'fullscreen':
|
|
1293
|
+
return ` </div>\n`;
|
|
1294
|
+
case 'aside':
|
|
1295
|
+
return ` </aside>\n`;
|
|
1296
|
+
case 'split':
|
|
1297
|
+
// The end pane (WorkspaceShell's `end`, opened above) is a fixed,
|
|
1298
|
+
// always-present projection point for `split` specifically (Task 12) —
|
|
1299
|
+
// orthogonal to Task 13's generic `slots` field, which still emits
|
|
1300
|
+
// above the chat pane the same as every other non-custom layout (see
|
|
1301
|
+
// emitApp). WorkspaceShell supplies its own real draggable splitter
|
|
1302
|
+
// between the two, so there is no hand-rolled two-column math left to
|
|
1303
|
+
// close here.
|
|
1304
|
+
return ` </WorkspaceShell>\n </div>\n`;
|
|
1305
|
+
case 'custom':
|
|
1306
|
+
return ''; // unreachable — see the block comment above
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// ── kai compile: the d.ts alongside the single .js ─────────────────────────
|
|
1311
|
+
|
|
1312
|
+
/** The declaration file `kai compile` writes beside the emitted .js — just
|
|
1313
|
+
* enough for a consumer's TS to know the tag and its one settable prop. */
|
|
1314
|
+
export function emitTypes(c: Construct): string {
|
|
1315
|
+
return `declare global {
|
|
1316
|
+
interface HTMLElementTagNameMap {
|
|
1317
|
+
'${c.name}': HTMLElement & { theme: 'light' | 'dark' | 'auto' };
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
export {};
|
|
1321
|
+
`;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// ── writing ──────────────────────────────────────────────────────────────────
|
|
1325
|
+
|
|
1326
|
+
const MANIFEST = '.kai-manifest.json';
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* Write files; prune anything the PREVIOUS generation wrote that this one
|
|
1330
|
+
* didn't. Returns the paths that already existed on disk before this write
|
|
1331
|
+
* (i.e. were overwritten) — callers that decide loudly (the CLI's `eject`)
|
|
1332
|
+
* use it to say so instead of silently clobbering a file the caller may have
|
|
1333
|
+
* hand-edited.
|
|
1334
|
+
*/
|
|
1335
|
+
export function writeProject(files: GeneratedFile[], dir: string): string[] {
|
|
1336
|
+
const manifestPath = join(dir, MANIFEST);
|
|
1337
|
+
const previous: string[] = existsSync(manifestPath)
|
|
1338
|
+
? (JSON.parse(readFileSync(manifestPath, 'utf8')) as string[])
|
|
1339
|
+
: [];
|
|
1340
|
+
const current = new Set(files.map((f) => f.path));
|
|
1341
|
+
for (const stale of previous) {
|
|
1342
|
+
if (!current.has(stale)) rmSync(join(dir, stale), { force: true });
|
|
1343
|
+
}
|
|
1344
|
+
const overwritten: string[] = [];
|
|
1345
|
+
for (const f of files) {
|
|
1346
|
+
const abs = join(dir, f.path);
|
|
1347
|
+
if (existsSync(abs)) overwritten.push(f.path);
|
|
1348
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
1349
|
+
writeFileSync(abs, f.code);
|
|
1350
|
+
}
|
|
1351
|
+
writeFileSync(manifestPath, `${JSON.stringify([...current].sort(), null, 2)}\n`);
|
|
1352
|
+
return overwritten;
|
|
1353
|
+
}
|