@kitn.ai/ui 0.25.1 → 0.26.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 +5 -7
- package/dist/components/attachment-types.d.ts +46 -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/chat-thread.d.ts +3 -41
- package/dist/components/code-block.d.ts +22 -0
- package/dist/components/conversation-item.d.ts +54 -1
- package/dist/components/conversation-list.d.ts +65 -0
- package/dist/components/form.d.ts +42 -0
- package/dist/components/message.d.ts +7 -0
- package/dist/components/toast.d.ts +2 -1
- 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/{create-tween-B5Y4Im8G.js → create-tween-BFT6c-Y8.js} +1 -1
- package/dist/{create-tween-4z1XYLkR.js → create-tween-CJNlzoaO.js} +1 -1
- package/dist/{create-tween-CUzOlaMS.js → create-tween-CQED4T0g.js} +1 -1
- package/dist/custom-elements.json +2931 -2572
- 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/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/checkpoint.js +1 -1
- package/dist/elements/choice.js +1 -1
- package/dist/elements/chunks/{Icon-b5A8hlHs.js → Icon--Y8vsFal.js} +1 -1
- package/dist/elements/chunks/action-icons-D9u3OzYW.js +1 -0
- package/dist/elements/chunks/arrow-left-CVh1TeN7.js +1 -0
- package/dist/elements/chunks/{artifact-BSuPYFHY.js → artifact-BkYcfIlZ.js} +1 -1
- package/dist/elements/chunks/attachments-DwfNlubq.js +1 -0
- package/dist/elements/chunks/audio-visualizer-D7lB6mvR.js +1 -0
- package/dist/elements/chunks/{badge-TIMiJFKc.js → badge-Q0-Cmwzz.js} +1 -1
- package/dist/elements/chunks/{button-B9okqG73.js → button-flUbeufF.js} +1 -1
- package/dist/elements/chunks/{card-renderer-CSoTL_e1.js → card-renderer-3Dyl-FpN.js} +1 -1
- package/dist/elements/chunks/check-B5kBPKeo.js +1 -0
- package/dist/elements/chunks/chevron-down-DZ1GS39h.js +1 -0
- package/dist/elements/chunks/chevron-right-CvP7OUd2.js +1 -0
- package/dist/elements/chunks/{choice-card-CGMc3kek.js → choice-card-D4QUIOWZ.js} +1 -1
- package/dist/elements/chunks/circle-CDf9g6MQ.js +1 -0
- package/dist/elements/chunks/circle-check-CA47Nqbz.js +1 -0
- package/dist/elements/chunks/{circle-x-mhH7pOK2.js → circle-x-CJ_HVbC8.js} +1 -1
- package/dist/elements/chunks/code-block-BdLiTitd.js +1 -0
- package/dist/elements/chunks/collapsible-BzbXpPzr.js +1 -0
- package/dist/elements/chunks/{composer-CZZmtYOT.js → composer-Byve-n8K.js} +2 -2
- package/dist/elements/chunks/{confirm-card-BWjf2-xo.js → confirm-card-B0flE3MN.js} +1 -1
- package/dist/elements/chunks/context-VLIL3sJ-.js +1 -0
- package/dist/elements/chunks/controllable-DTwTIlwp.js +1 -0
- package/dist/elements/chunks/conversation-item-BDztezOG.js +1 -0
- package/dist/elements/chunks/copy-Nf-kqxXH.js +1 -0
- package/dist/elements/chunks/{create-tween-CUrM6D_o.js → create-tween-c4Z8TDYz.js} +2 -2
- package/dist/elements/chunks/default-input-C2iLFq0b.js +1 -0
- package/dist/elements/chunks/define-wjHR1MkU.js +1 -0
- package/dist/elements/chunks/disclosure-CcNbyLy0.js +1 -0
- package/dist/elements/chunks/{download-DnCyWFJE.js → download-C4V7tBdb.js} +1 -1
- package/dist/elements/chunks/dropdown-BgWTthUx.js +1 -0
- package/dist/elements/chunks/{ellipsis-CVxqfsDZ.js → ellipsis-7gU33mL2.js} +1 -1
- package/dist/elements/chunks/embed-D3KewsIJ.js +1 -0
- package/dist/elements/chunks/{external-link-DG2sRdQL.js → external-link-CSOEsNlx.js} +1 -1
- package/dist/elements/chunks/{file-text-D0J9F6M7.js → file-text-D1G8khle.js} +1 -1
- package/dist/elements/chunks/{file-tree-yGWNK0Js.js → file-tree-zvB2i7P9.js} +1 -1
- package/dist/elements/chunks/{folder-NV1pi_ud.js → folder-WW2LGLid.js} +1 -1
- package/dist/elements/chunks/form-CQirdC4W.js +1 -0
- package/dist/elements/chunks/hover-card-CJJkfHTK.js +1 -0
- package/dist/elements/chunks/icon-Cfgmf4fa.js +1 -0
- package/dist/elements/chunks/{info-C8r5Q6cc.js → info-DX3YMOt6.js} +1 -1
- package/dist/elements/chunks/input-BtW9U6Sq.js +1 -0
- package/dist/elements/chunks/{kbd-OGJDUT-O.js → kbd-LGXwuT_V.js} +1 -1
- package/dist/elements/chunks/{link-BdsfLB7b.js → link-2My69BqT.js} +1 -1
- package/dist/elements/chunks/{link-preview-Cf5KOP-i.js → link-preview-OQ9IEq0f.js} +1 -1
- package/dist/elements/chunks/{loader-B5QzK_8_.js → loader-iOxc_Tu-.js} +1 -1
- package/dist/elements/chunks/{markdown-DbozyYqa.js → markdown-DgBFaGAz.js} +1 -1
- package/dist/elements/chunks/{message-DtXKPqiR.js → message-VMZohrXx.js} +1 -1
- package/dist/elements/chunks/message-circle-CpXMM-_n.js +1 -0
- package/dist/elements/chunks/message-square-DxcWtETQ.js +1 -0
- package/dist/elements/chunks/message-tZmV2A3p.js +1 -0
- package/dist/elements/chunks/{minimize-2-SegJ_oku.js → minimize-2-DPIUKTrT.js} +1 -1
- package/dist/elements/chunks/{model-switcher-D7JrFFHp.js → model-switcher-DsT8tFB0.js} +1 -1
- package/dist/elements/chunks/{overlay-CV7z2YuX.js → overlay-DuiwdXjm.js} +1 -1
- package/dist/elements/chunks/{paperclip-Bg0ITU3b.js → paperclip-CRZVp4z_.js} +1 -1
- package/dist/elements/chunks/{progress-bar-D1_rzt6N.js → progress-bar-DNy66e_E.js} +1 -1
- package/dist/elements/chunks/{prompt-suggestion-D5Sm8xF1.js → prompt-suggestion-Bn64Nl7L.js} +1 -1
- package/dist/elements/chunks/reasoning-Emn9N4-T.js +1 -0
- package/dist/elements/chunks/{resizable-Dpzjlvp4.js → resizable-D181b4fM.js} +1 -1
- package/dist/elements/chunks/{rotate-cw-DzVDJOA1.js → rotate-cw-CPWe37D9.js} +1 -1
- package/dist/elements/chunks/{scroll-area-93A1bbeG.js → scroll-area-ClZkZxIk.js} +1 -1
- package/dist/elements/chunks/scroll-button-DPE-t3x_.js +1 -0
- package/dist/elements/chunks/{separator-DH2r_ZSZ.js → separator-CeJHQqLW.js} +1 -1
- package/dist/elements/chunks/{settings-BFMOd6yB.js → settings-CqaHgJF9.js} +1 -1
- package/dist/elements/chunks/{settings-group-B3VZKIcz.js → settings-group-CjqjLCOn.js} +1 -1
- package/dist/elements/chunks/share-CyfnNEqp.js +1 -0
- package/dist/elements/chunks/{skeleton-N8BuWYmf.js → skeleton-D8akBnKS.js} +1 -1
- package/dist/elements/chunks/slots-DaNjX0rc.js +1 -0
- package/dist/elements/chunks/{source-w9V0NnBn.js → source-UWCvmVak.js} +1 -1
- package/dist/elements/chunks/{star-DHvACxtm.js → star-DoHFAikQ.js} +1 -1
- package/dist/elements/chunks/store-Cjq0qwYN.js +1 -0
- package/dist/elements/chunks/{tasks-card-lPxoodlA.js → tasks-card-CmZ5xh6v.js} +1 -1
- package/dist/elements/chunks/{text-shimmer-BY_rI3s5.js → text-shimmer-DYn09zrz.js} +1 -1
- package/dist/elements/chunks/textarea-skiGHURM.js +1 -0
- package/dist/elements/chunks/{thumbs-up-DQe0hvkZ.js → thumbs-up-DX2Ijk3H.js} +1 -1
- package/dist/elements/chunks/{toast-store-VUZZBhzH.js → toast-store-CFnQpThX.js} +1 -1
- package/dist/elements/chunks/{tool-C1YtxwPu.js → tool-DKQ2D1DP.js} +1 -1
- package/dist/elements/chunks/{tooltip-BqwnFWhn.js → tooltip-B0G99y8w.js} +1 -1
- package/dist/elements/chunks/{triangle-alert-B6or7F5u.js → triangle-alert-F9cyUEyy.js} +1 -1
- package/dist/elements/chunks/{use-card-resolution-DJg32jGH.js → use-card-resolution-DoA2f_EU.js} +1 -1
- package/dist/elements/chunks/{variant-aurora-QXTZPyST.js → variant-aurora-qt6Zct8M.js} +2 -2
- package/dist/elements/chunks/variant-custom-tJp22_BQ.js +1 -0
- package/dist/{variant-wave-BwzrHD1s.js → elements/chunks/variant-wave-CaQ5ti39.js} +2 -2
- package/dist/elements/chunks/x-B6UnOiLw.js +1 -0
- 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.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 +149 -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-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-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/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/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/slots.d.ts +35 -3
- 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 +232 -148
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5456 -4931
- package/dist/index.server.js +4142 -3669
- package/dist/kai-provider.es.js +40 -40
- package/dist/kai.es.js +1 -1
- package/dist/llms/llms-full.txt +184 -69
- package/dist/llms/llms.txt +3 -3
- package/dist/mcp.es.js +2669 -546
- 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/react/index.d.ts +115 -266
- package/dist/react.js +247 -221
- package/dist/register-impl-DpE7icIb.js +293 -0
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/tool-defs.d.ts +35 -0
- package/dist/schemas.js +403 -310
- package/dist/{solid-BMuDmFo_.js → solid-BtdqNZI7.js} +8435 -7394
- package/dist/{solid-CGYNdLRR.js → solid-CnavwBye.js} +5987 -5035
- package/dist/solid.d.ts +5 -0
- package/dist/solid.js +210 -203
- package/dist/solid.server.js +210 -203
- package/dist/state/index.d.ts +4 -0
- 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 +269 -169
- package/dist/types.d.ts +4 -2
- package/dist/ui/dock.d.ts +109 -0
- package/dist/ui/hover-card.d.ts +44 -0
- package/dist/{variant-aurora-DwSOfraQ.js → variant-aurora-B8II3-I1.js} +27 -27
- package/dist/{variant-aurora-wKvAQ5wp.js → variant-aurora-CMNWOSJR.js} +28 -28
- package/dist/{variant-aurora-XEkPT0_z.js → variant-aurora-CcuDMWwQ.js} +2 -2
- package/dist/{variant-custom-eFIg2XWQ.js → variant-custom-BdvYC7af.js} +40 -40
- package/dist/{variant-custom-BhS9wIpU.js → variant-custom-C714mqKl.js} +65 -65
- package/dist/variant-custom-L16JLLZy.js +1 -0
- package/dist/{elements/chunks/variant-wave-DDCwhLas.js → variant-wave-2S4La2Ka.js} +2 -2
- package/dist/{variant-wave-CbVsmgwT.js → variant-wave-D7ruiA1W.js} +25 -25
- package/dist/{variant-wave-CUWGx9Bn.js → variant-wave-ecwykeyL.js} +21 -21
- 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/frameworks/react/index.tsx +132 -93
- package/llms-full.txt +184 -69
- package/llms.txt +3 -3
- package/package.json +27 -7
- 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 +275 -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 +61 -5
- package/src/agent-tooling/mcp/tools/reference.ts +440 -4
- 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/registry.ts +19 -7
- package/src/agent-tooling/route-emit.ts +26 -2
- package/src/components/attachment-types.ts +47 -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/chat-container.tsx +4 -1
- package/src/components/chat-thread.tsx +36 -7
- package/src/components/code-block.tsx +79 -3
- package/src/components/composer.tsx +8 -1
- package/src/components/conversation-item.tsx +114 -4
- package/src/components/conversation-list.tsx +212 -12
- package/src/components/embed.tsx +4 -0
- package/src/components/form.tsx +41 -14
- package/src/components/message.tsx +75 -5
- package/src/components/response-stream.tsx +13 -7
- package/src/components/screen.tsx +5 -0
- package/src/components/scroll-button.tsx +7 -0
- package/src/components/toast.tsx +11 -5
- 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 +6 -7
- package/src/elements/audio-visualizer.tsx +48 -4
- package/src/elements/chat-workspace.tsx +159 -287
- package/src/elements/chat.tsx +12 -9
- 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/conversation-item.tsx +80 -0
- package/src/elements/conversation-list.tsx +60 -11
- package/src/elements/default-input.tsx +5 -5
- 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 +204 -0
- package/src/elements/dropdown.tsx +123 -0
- package/src/elements/element-diagnostics.ts +392 -0
- package/src/elements/element-manifest.json +12 -0
- package/src/elements/element-meta.json +672 -439
- package/src/elements/element-nonscalar.json +170 -0
- package/src/elements/element-types.d.ts +232 -148
- package/src/elements/menu.tsx +13 -0
- package/src/elements/prompt-dock.tsx +2 -1
- package/src/elements/prompt-input.tsx +8 -8
- package/src/elements/register-impl.ts +46 -0
- package/src/elements/register.ts +28 -0
- package/src/elements/resizable.tsx +52 -25
- package/src/elements/scroll-button.tsx +5 -0
- package/src/elements/slots.ts +107 -12
- package/src/elements/styles.css +30 -6
- package/src/elements/thinking-bar.tsx +16 -7
- package/src/elements/toast.tsx +3 -0
- 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 +2 -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 +297 -6
- package/src/solid.ts +5 -0
- package/src/state/index.ts +12 -0
- 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/dialog.tsx +6 -0
- package/src/ui/dock.tsx +445 -0
- package/src/ui/dropdown.tsx +67 -23
- package/src/ui/hover-card.tsx +156 -6
- package/src/ui/input.tsx +29 -10
- package/src/ui/resizable.tsx +11 -0
- 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/dist/elements/chunks/action-icons-DW9muWrY.js +0 -1
- package/dist/elements/chunks/arrow-left-iJVnQmxA.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/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/circle-BG7VkPep.js +0 -1
- package/dist/elements/chunks/circle-check-GqvO2UBs.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/context-hKjNqiyQ.js +0 -1
- package/dist/elements/chunks/conversation-list-CsGJjSKG.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/dropdown-CmNna5lT.js +0 -1
- package/dist/elements/chunks/embed-CjAD2jSi.js +0 -1
- package/dist/elements/chunks/form-Cp8Y00Wn.js +0 -1
- package/dist/elements/chunks/hover-card-6KtXpLwT.js +0 -1
- package/dist/elements/chunks/icon-97R8SR2p.js +0 -1
- package/dist/elements/chunks/input-DrseEjhp.js +0 -1
- package/dist/elements/chunks/message-CE4k-qaj.js +0 -1
- package/dist/elements/chunks/message-square-C9ovmtOo.js +0 -1
- package/dist/elements/chunks/reasoning-BrxQfpfI.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/store-HIPGhNz9.js +0 -1
- package/dist/elements/chunks/textarea-DPfuhswM.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/x-BOhjBeOd.js +0 -1
- package/dist/primitives/card-validate-generator.testlib.d.ts +0 -20
- package/dist/register-impl-DGmGzrEM.js +0 -145
- package/dist/variant-custom-MkTNTycd.js +0 -1
- package/src/primitives/card-validate-generator.testlib.ts +0 -38
package/dist/mcp.es.js
CHANGED
|
@@ -1,79 +1,80 @@
|
|
|
1
1
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
4
5
|
import { z as z$1 } from "zod";
|
|
5
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { join,
|
|
8
|
-
var
|
|
9
|
-
var
|
|
10
|
-
var w = (e, t, i) =>
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
$schema:
|
|
14
|
-
$id:
|
|
15
|
-
title:
|
|
16
|
-
description:
|
|
17
|
-
type:
|
|
8
|
+
import { join, dirname, resolve } from "node:path";
|
|
9
|
+
var z = Object.defineProperty;
|
|
10
|
+
var M = (e, t, i) => t in e ? z(e, t, { enumerable: true, configurable: true, writable: true, value: i }) : e[t] = i;
|
|
11
|
+
var w = (e, t, i) => M(e, typeof t != "symbol" ? t + "" : t, i);
|
|
12
|
+
const U = "https://json-schema.org/draft/2020-12/schema", F = "https://kitn.ai/schemas/card/artifact.schema.json", J = "ArtifactCardData", B = "Generated-artifact viewer payload: a framed preview plus the source files behind it. A deliberately NARROW subset of the Artifact component's props — it says WHAT to show, never how the viewer behaves. Toolbar composition, view-state and the iframe `sandbox` are host-owned and stay off the wire, so a model cannot widen its own sandbox or hide the chrome the user needs to inspect what it built.", Y = "object";
|
|
13
|
+
const H = [{ required: ["src"] }, { required: ["files"] }], K = { src: { type: "string", format: "uri", description: "URL the preview iframe frames, and the target of the Home button. Omit for a source-only artifact (`tab: 'code'` with `files`).", "x-kai-format": "url" }, files: { type: "array", description: "Files behind the artifact: the Code tab's tree, each optionally carrying its own preview url and source.", maxItems: 500, items: { type: "object", required: ["path"], additionalProperties: false, properties: { path: { type: "string", description: "Tree label/key. Folders are built from `/`-delimited segments.", maxLength: 1024 }, url: { type: "string", format: "uri", description: "Where the preview loads this file. Defaults to `path` resolved against `src`.", "x-kai-format": "url" }, code: { type: "string", description: "Source shown in the Code tab. Omit for binary files (image/pdf), which have no code view." }, language: { type: "string", description: "Language id for syntax highlighting (e.g. 'html', 'css', 'tsx').", maxLength: 40 }, type: { type: "string", enum: ["html", "pdf", "image", "other"], description: "File kind. Drives the tree icon and whether the Code view applies; 'pdf' also routes the preview to the inline PDF viewer.", "x-kai-control": "select" }, additions: { type: "integer", minimum: 0, description: "Lines added vs the base. Rendered as a trailing '+N' stat." }, deletions: { type: "integer", minimum: 0, description: "Lines removed vs the base. Rendered as a trailing '-N' stat." }, status: { type: "string", enum: ["added", "modified", "deleted", "renamed", "untracked"], description: "Change status vs the base. Drives a trailing status letter in the conventional VCS hue.", "x-kai-control": "select" } } } }, tab: { type: "string", enum: ["preview", "code"], default: "preview", description: "Which view the card OPENS on. SEED ONLY: the user can switch freely afterwards and a revision of this card will NOT move them back, because the user's choice wins over the model's. A switch is reported back as a `state` patch.", "x-kai-control": "select" }, activeFile: { type: "string", description: "Path of the file selected in the tree when the card FIRST renders (must match a `files[].path`). Seeds the tree highlight, the Code source and the preview. SEED ONLY, like `tab`: revising this card will not move a user who has since selected a different file.", maxLength: 1024 }, displayUrl: { type: "string", description: "Friendly address shown in the path field INSTEAD of the real url, which also makes the field read-only. Use when `src` is not consumer-facing (e.g. a `data:` blob) so a clean address is shown rather than leaking it.", maxLength: 2048 }, height: { description: "Frame height. A bare number is px; a string is any CSS length. Defaults to 420px. REQUIRED CONCEPT, not decoration: the viewer is built to fill its container, and a message thread supplies no height, so the card sets one explicitly or the artifact collapses to zero and renders invisible.", oneOf: [{ type: "number", minimum: 1 }, { type: "string", maxLength: 32 }] } }, W = {
|
|
14
|
+
$schema: U,
|
|
15
|
+
$id: F,
|
|
16
|
+
title: J,
|
|
17
|
+
description: B,
|
|
18
|
+
type: Y,
|
|
18
19
|
additionalProperties: false,
|
|
19
|
-
anyOf:
|
|
20
|
-
properties:
|
|
21
|
-
},
|
|
22
|
-
$schema:
|
|
23
|
-
$id:
|
|
24
|
-
title:
|
|
25
|
-
description:
|
|
26
|
-
type:
|
|
27
|
-
required:
|
|
28
|
-
properties:
|
|
29
|
-
},
|
|
30
|
-
$schema:
|
|
31
|
-
$id:
|
|
32
|
-
title:
|
|
33
|
-
description:
|
|
34
|
-
type:
|
|
35
|
-
required:
|
|
36
|
-
properties:
|
|
37
|
-
},
|
|
38
|
-
$schema:
|
|
39
|
-
$id:
|
|
40
|
-
title:
|
|
41
|
-
description:
|
|
42
|
-
type:
|
|
43
|
-
required:
|
|
44
|
-
additionalProperties:
|
|
45
|
-
properties:
|
|
46
|
-
allOf:
|
|
47
|
-
},
|
|
48
|
-
$schema:
|
|
49
|
-
$id:
|
|
50
|
-
title:
|
|
51
|
-
description:
|
|
52
|
-
type:
|
|
53
|
-
required:
|
|
54
|
-
properties:
|
|
55
|
-
},
|
|
56
|
-
$schema:
|
|
57
|
-
$id:
|
|
58
|
-
title:
|
|
59
|
-
description:
|
|
60
|
-
type:
|
|
61
|
-
required:
|
|
62
|
-
additionalProperties:
|
|
63
|
-
properties:
|
|
64
|
-
},
|
|
65
|
-
$schema:
|
|
66
|
-
$id:
|
|
67
|
-
title:
|
|
68
|
-
description:
|
|
69
|
-
type:
|
|
70
|
-
required:
|
|
71
|
-
properties:
|
|
20
|
+
anyOf: H,
|
|
21
|
+
properties: K
|
|
22
|
+
}, ue = "https://json-schema.org/draft/2020-12/schema", le = "https://kitn.ai/schemas/card/choice.schema.json", me = "ChoiceCardData", he = "Data payload for a `choice` card (CardEnvelope.data when type='choice'). A single-select 'pick one of N rich options' card; activating an option emits the `action` verb.", fe = "object", ye = ["options"], ge = { prompt: { type: "string", description: "Optional question/body text rendered above the options. Distinct from CardEnvelope.title (the card chrome heading)." }, submitLabel: { type: "string", description: "Label for the Submit button; default 'Submit'." }, options: { type: "array", minItems: 1, description: "The selectable options, rendered in order. Exactly one can be chosen.", items: { type: "object", required: ["id", "label"], properties: { id: { type: "string", minLength: 1, description: "Emitted as CardEvent.action. Must be unique within `options`. (The reserved id '__other__' is used by the allowOther escape.)", "x-kai-unique": true }, label: { type: "string", minLength: 1, description: "Visible option label." }, description: { type: "string", description: "Optional secondary line under the label." }, media: { type: "object", description: "Optional leading media: an image URL or a named icon.", properties: { image: { type: "string", description: "Image URL rendered as a leading thumbnail in the row." }, imageAlt: { type: "string", description: "Accessible alt text for `image` (decorative if omitted)." }, icon: { type: "string", description: "Named icon rendered as a leading glyph." } } }, meta: { type: "string", description: "Trailing freeform text (e.g. a price or badge), right-aligned in the row." }, recommended: { type: "boolean", default: false, description: "Renders a small 'Recommended' pill near the label." }, disabled: { type: "boolean", default: false, description: "Option is shown but not selectable, and skipped in keyboard nav." }, payload: { description: "Optional opaque payload echoed back in CardEvent.payload (any JSON)." } } } }, allowOther: { description: "Optional free-text escape ('none of these / specify'). `true` appends a final 'Other…' option; an object customizes its `label`/`placeholder`. Selecting it reveals a text input; the shared Submit button then emits `{ kind:'action', action:'__other__', payload:{ text } }`.", "x-kai-control": "allow-other" }, dismissible: { type: "boolean", default: false, description: "Show a close affordance that emits the `dismiss` verb. The card collapses to a re-openable 'dismissed' stub; the host decides via `onReopen` whether it comes back live or `expired`." } }, be = {
|
|
23
|
+
$schema: ue,
|
|
24
|
+
$id: le,
|
|
25
|
+
title: me,
|
|
26
|
+
description: he,
|
|
27
|
+
type: fe,
|
|
28
|
+
required: ye,
|
|
29
|
+
properties: ge
|
|
30
|
+
}, ve = "https://json-schema.org/draft/2020-12/schema", $e = "https://kitn.ai/schemas/card/confirm.schema.json", we = "ConfirmCardData", xe = "Data payload for a `confirm` card (CardEnvelope.data when type='confirm').", ke = "object", Oe = ["actions"], je = { heading: { type: "string", description: "Optional in-body heading. Distinct from CardEnvelope.title (the card chrome title)." }, body: { type: "string", description: "Body text. Plain text in v1 (rendered safely; no HTML injection)." }, tone: { type: "string", enum: ["default", "warning", "danger"], default: "default", description: "Overall card tone; 'danger' adds a warning icon + accent for destructive approvals.", "x-kai-control": "tone" }, actions: { type: "array", minItems: 1, maxItems: 4, description: "The choice set. Rendered as buttons in order; max 4 keeps it a decision, not a menu.", items: { type: "object", required: ["id", "label"], properties: { id: { type: "string", minLength: 1, description: "Emitted as CardEvent.action. Must be unique within `actions`.", "x-kai-unique": true }, label: { type: "string", minLength: 1, description: "Visible button label." }, style: { type: "string", enum: ["primary", "default", "destructive"], default: "default", description: "Button emphasis. 'destructive' = red/danger; 'primary' = filled accent.", "x-kai-control": "button-style" }, payload: { description: "Optional opaque payload echoed back in CardEvent.payload (any JSON)." }, default: { type: "boolean", default: false, description: "If true, this action is the keyboard default (Enter) and gets initial focus. At most one should be true; the card uses the first if several are.", "x-kai-default-action": true } } } }, dismissible: { type: "boolean", default: false, description: "Show a close affordance that emits the `dismiss` verb." } }, Se = {
|
|
31
|
+
$schema: ve,
|
|
32
|
+
$id: $e,
|
|
33
|
+
title: we,
|
|
34
|
+
description: xe,
|
|
35
|
+
type: ke,
|
|
36
|
+
required: Oe,
|
|
37
|
+
properties: je
|
|
38
|
+
}, Ae = "https://json-schema.org/draft/2020-12/schema", qe = "https://kitn.ai/schemas/card/embed.schema.json", Ie = "EmbedCardData", Le = "Lazy media-embed payload (YouTube / Vimeo / generic player URL).", Ee = "object", Te = ["provider"], Ce = false, Re = { provider: { type: "string", enum: ["youtube", "vimeo", "generic"], description: "Media provider. 'generic' frames `url` directly (must be an https embeddable player URL whose origin is app-allowlisted).", "x-kai-control": "select" }, id: { type: "string", description: "Provider video id (required for youtube/vimeo when `url` is absent). e.g. 'dQw4w9WgXcQ'.", maxLength: 64, pattern: "^[A-Za-z0-9_-]+$" }, url: { type: "string", format: "uri", description: "Full media/watch URL. For youtube/vimeo it is parsed to an id; for 'generic' it is the embeddable player src (https only).", "x-kai-format": "url" }, title: { type: "string", description: "Accessible title for the player iframe + the poster label. Strongly recommended for a11y.", maxLength: 300 }, poster: { type: "string", format: "uri", description: "Thumbnail shown before play. When omitted, youtube/vimeo derive a default thumbnail; 'generic' shows a neutral play placeholder.", "x-kai-format": "url" }, start: { type: "integer", minimum: 0, description: "Optional start offset in seconds.", "x-kai-unit": "seconds" }, aspectRatio: { type: "string", enum: ["16:9", "4:3", "1:1", "9:16"], default: "16:9", description: "Player box aspect ratio (CSS aspect-ratio).", "x-kai-control": "select" } }, Ne = [{ if: { properties: { provider: { const: "generic" } } }, then: { required: ["url"] } }, { if: { properties: { provider: { enum: ["youtube", "vimeo"] } } }, then: { anyOf: [{ required: ["id"] }, { required: ["url"] }] } }], Pe = {
|
|
39
|
+
$schema: Ae,
|
|
40
|
+
$id: qe,
|
|
41
|
+
title: Ie,
|
|
42
|
+
description: Le,
|
|
43
|
+
type: Ee,
|
|
44
|
+
required: Te,
|
|
45
|
+
additionalProperties: Ce,
|
|
46
|
+
properties: Re,
|
|
47
|
+
allOf: Ne
|
|
48
|
+
}, Je = "https://json-schema.org/draft/2020-12/schema", Be = "https://kitn.ai/schemas/card/form.schema.json", Ye = "kai-form data (a form definition)", He = "The CardEnvelope.data for a card of type 'form': a JSON Schema (type:'object') describing the fields kai-form renders, plus optional x-kai-* UI hints. The same schema validates the submission.", Ke = "object", We = ["type", "properties"], Ve = { type: { const: "object" }, title: { type: "string" }, description: { type: "string" }, required: { type: "array", items: { type: "string" } }, properties: { type: "object" }, "x-kai-order": { type: "array", items: { type: "string" } }, "x-kai-inlineMax": { type: "integer", minimum: 1 }, "x-kai-submitLabel": { type: "string" }, "x-kai-dismissible": { type: "boolean" }, "x-kai-actions": { type: "array", items: { type: "object", required: ["id", "label"], properties: { id: { type: "string" }, label: { type: "string" }, variant: { enum: ["default", "ghost", "outline"] } } } } }, Ge = {
|
|
49
|
+
$schema: Je,
|
|
50
|
+
$id: Be,
|
|
51
|
+
title: Ye,
|
|
52
|
+
description: He,
|
|
53
|
+
type: Ke,
|
|
54
|
+
required: We,
|
|
55
|
+
properties: Ve
|
|
56
|
+
}, Xe = "https://json-schema.org/draft/2020-12/schema", Qe = "https://kitn.ai/schemas/card/link.schema.json", Ze = "LinkPreviewData", et = "Rich link / Open-Graph preview payload. The card renders from this; it never fetches.", tt = "object", it = ["url"], nt = false, ot = { url: { type: "string", format: "uri", description: "Canonical destination. Opened via the contract `open` verb (target 'tab').", "x-kai-format": "url" }, title: { type: "string", description: "OG title (og:title). Falls back to the domain when absent.", maxLength: 300 }, description: { type: "string", description: "OG description (og:description). Clamped to 3 lines in the UI.", maxLength: 1e3 }, image: { type: "string", format: "uri", description: "Preview image (og:image). Optional; the card degrades gracefully when missing or it fails to load.", "x-kai-format": "url" }, imageAlt: { type: "string", description: "Alt text for the preview image. Defaults to the title (or empty = decorative) when omitted.", maxLength: 300 }, favicon: { type: "string", format: "uri", description: "Site favicon shown next to the domain.", "x-kai-format": "url" }, domain: { type: "string", description: "Display domain (e.g. 'example.com'). Derived from `url` when omitted.", maxLength: 253 }, siteName: { type: "string", description: "OG site name (og:site_name). Shown in place of the domain when present.", maxLength: 200 } }, rt = {
|
|
57
|
+
$schema: Xe,
|
|
58
|
+
$id: Qe,
|
|
59
|
+
title: Ze,
|
|
60
|
+
description: et,
|
|
61
|
+
type: tt,
|
|
62
|
+
required: it,
|
|
63
|
+
additionalProperties: nt,
|
|
64
|
+
properties: ot
|
|
65
|
+
}, ht = "https://json-schema.org/draft/2020-12/schema", ft = "https://kitn.ai/schemas/card/tasks.schema.json", yt = "TasksCardData", gt = "Data payload for a `tasks` card (CardEnvelope.data when type='tasks').", bt = "object", vt = ["tasks"], $t = { mode: { type: "string", enum: ["select", "progress"], default: "select", description: "'select' (default) = toggle + a confirm button that emits `submit`. 'progress' = an onboarding-checklist look (header `done / total` count, circular indicators, per-item title/description, no confirm button, where checking a row IS the action). Both share the selection model.", "x-kai-mode": true }, heading: { type: "string", description: "Optional in-body heading; distinct from CardEnvelope.title." }, tasks: { type: "array", minItems: 1, description: "The selectable rows, rendered in order.", items: { type: "object", required: ["id", "label"], properties: { id: { type: "string", minLength: 1, description: "Stable id; the selected ids are returned in the result. Unique within `tasks`.", "x-kai-unique": true }, label: { type: "string", minLength: 1, description: "Row label." }, description: { type: "string", description: "Optional secondary line under the label." }, checked: { type: "boolean", default: false, description: "Initial checked state of the row." }, disabled: { type: "boolean", default: false, description: "Row is shown but not toggleable (and excluded from select-all)." } } } }, selectAll: { type: "boolean", default: false, description: "Render a master select-all checkbox above the list.", "x-kai-control": "select-all" }, confirmLabel: { type: "string", default: "Confirm", description: "Label for the confirm button." }, allowEmpty: { type: "boolean", default: false, description: "If true, confirm is enabled with zero selected (emits { selected: [] }). If false, confirm is disabled until >=1 selected." }, min: { type: "integer", minimum: 0, description: "Optional minimum number that must be selected to confirm.", "x-kai-select-min": true }, max: { type: "integer", minimum: 1, description: "Optional maximum selectable; further toggles are blocked once reached.", "x-kai-select-max": true }, dismissible: { type: "boolean", default: false, description: "Show a close affordance that emits the `dismiss` verb. The card collapses to a re-openable 'dismissed' stub; the host decides via `onReopen` whether it comes back live or `expired`." } }, wt = {
|
|
66
|
+
$schema: ht,
|
|
67
|
+
$id: ft,
|
|
68
|
+
title: yt,
|
|
69
|
+
description: gt,
|
|
70
|
+
type: bt,
|
|
71
|
+
required: vt,
|
|
72
|
+
properties: $t
|
|
72
73
|
}, S = "kai_";
|
|
73
|
-
function
|
|
74
|
+
function R(e) {
|
|
74
75
|
return `${S}${e}`;
|
|
75
76
|
}
|
|
76
|
-
const
|
|
77
|
+
const N = (e) => Object.freeze(e), P = Object.freeze({
|
|
77
78
|
id: "openai",
|
|
78
79
|
label: "OpenAI strict",
|
|
79
80
|
source: "https://developers.openai.com/api/docs/guides/structured-outputs",
|
|
@@ -82,7 +83,7 @@ const L = (e) => Object.freeze(e), C = Object.freeze({
|
|
|
82
83
|
requireAllPropertiesRequired: true,
|
|
83
84
|
allowRootAnyOf: false,
|
|
84
85
|
allowRecursion: true,
|
|
85
|
-
keywords:
|
|
86
|
+
keywords: N({
|
|
86
87
|
// core
|
|
87
88
|
type: { status: "supported", note: "String, Number, Boolean, Integer, Object, Array are the supported types" },
|
|
88
89
|
properties: { status: "supported", note: "objects are a supported type" },
|
|
@@ -161,7 +162,7 @@ const L = (e) => Object.freeze(e), C = Object.freeze({
|
|
|
161
162
|
dependentRequired: { status: "unsupported", note: "explicitly listed as not supported under Composition" },
|
|
162
163
|
dependentSchemas: { status: "unsupported", note: "explicitly listed as not supported under Composition" }
|
|
163
164
|
})
|
|
164
|
-
}),
|
|
165
|
+
}), _ = Object.freeze({
|
|
165
166
|
id: "anthropic",
|
|
166
167
|
label: "Anthropic strict",
|
|
167
168
|
source: "https://platform.claude.com/docs/en/build-with-claude/structured-outputs#json-schema-limitations",
|
|
@@ -171,7 +172,7 @@ const L = (e) => Object.freeze(e), C = Object.freeze({
|
|
|
171
172
|
requireAllPropertiesRequired: false,
|
|
172
173
|
allowRootAnyOf: true,
|
|
173
174
|
allowRecursion: false,
|
|
174
|
-
keywords:
|
|
175
|
+
keywords: N({
|
|
175
176
|
// core
|
|
176
177
|
type: { status: "supported", note: "all basic types: object, array, string, integer, number, boolean, null" },
|
|
177
178
|
properties: { status: "supported", note: "objects are a supported type" },
|
|
@@ -251,90 +252,90 @@ const L = (e) => Object.freeze(e), C = Object.freeze({
|
|
|
251
252
|
dependentRequired: { status: "unsupported", note: "not in the supported list" },
|
|
252
253
|
dependentSchemas: { status: "unsupported", note: "not in the supported list" }
|
|
253
254
|
})
|
|
254
|
-
}),
|
|
255
|
-
function
|
|
255
|
+
}), xt = ["items", "if", "then", "else", "not", "contains", "propertyNames", "unevaluatedItems", "additionalItems"], kt = ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"], Ot = ["allOf", "anyOf", "oneOf", "prefixItems"], O = (e) => typeof e == "object" && e !== null && !Array.isArray(e);
|
|
256
|
+
function jt(e, t) {
|
|
256
257
|
return e === "(root)" ? t : `${e}.${t}`;
|
|
257
258
|
}
|
|
258
|
-
function
|
|
259
|
+
function St(e, t) {
|
|
259
260
|
const i = [];
|
|
260
261
|
return x(e, t, "(root)", true, i), i;
|
|
261
262
|
}
|
|
262
|
-
function x(e, t, i,
|
|
263
|
+
function x(e, t, i, n, r) {
|
|
263
264
|
if (typeof e == "boolean") return;
|
|
264
265
|
if (!O(e)) {
|
|
265
|
-
|
|
266
|
+
r.push({ path: i, keyword: "(schema)", reason: `expected a schema object, got ${e === null ? "null" : typeof e}` });
|
|
266
267
|
return;
|
|
267
268
|
}
|
|
268
269
|
for (const [p, c] of Object.entries(e)) {
|
|
269
|
-
const
|
|
270
|
-
if (!
|
|
271
|
-
|
|
270
|
+
const l = t.keywords[p];
|
|
271
|
+
if (!l) {
|
|
272
|
+
r.push({
|
|
272
273
|
path: i,
|
|
273
274
|
keyword: p,
|
|
274
275
|
reason: p.startsWith("x-") ? `vendor extension keyword, not in the ${t.label} subset` : `unknown keyword, not classified by the ${t.label} subset`
|
|
275
276
|
});
|
|
276
277
|
continue;
|
|
277
278
|
}
|
|
278
|
-
if (
|
|
279
|
-
|
|
279
|
+
if (l.status === "unsupported") {
|
|
280
|
+
r.push({ path: i, keyword: p, reason: `not supported (${l.note})` });
|
|
280
281
|
continue;
|
|
281
282
|
}
|
|
282
|
-
if (
|
|
283
|
-
const h =
|
|
284
|
-
h &&
|
|
283
|
+
if (l.status === "supported" && l.value) {
|
|
284
|
+
const h = l.value(c);
|
|
285
|
+
h && r.push({ path: i, keyword: p, reason: h });
|
|
285
286
|
}
|
|
286
287
|
}
|
|
287
|
-
const
|
|
288
|
-
if (
|
|
288
|
+
const o = O(e.properties), a = e.type === "object" || o;
|
|
289
|
+
if (a && t.requireAdditionalPropertiesFalse && e.additionalProperties !== false && r.push({
|
|
289
290
|
path: i,
|
|
290
291
|
keyword: "additionalProperties",
|
|
291
292
|
reason: `missing; ${t.label} requires \`additionalProperties: false\` on every object`
|
|
292
|
-
}),
|
|
293
|
+
}), a && !o && r.push({
|
|
293
294
|
path: i,
|
|
294
295
|
keyword: "properties",
|
|
295
296
|
reason: `a free-form object (\`type: "object"\` with no \`properties\`) has no grammar under ${t.label}; with the mandatory \`additionalProperties: false\` it can only ever be \`{}\`, which is not what the schema means`
|
|
296
|
-
}),
|
|
297
|
-
const p = Array.isArray(e.required) ? e.required.map(String) : [], c = Object.keys(e.properties).filter((
|
|
298
|
-
c.length > 0 &&
|
|
297
|
+
}), a && t.requireAllPropertiesRequired && o) {
|
|
298
|
+
const p = Array.isArray(e.required) ? e.required.map(String) : [], c = Object.keys(e.properties).filter((l) => !p.includes(l));
|
|
299
|
+
c.length > 0 && r.push({
|
|
299
300
|
path: i,
|
|
300
301
|
keyword: "required",
|
|
301
|
-
reason: `${t.label} requires every property in \`required\`; missing ${c.map((
|
|
302
|
+
reason: `${t.label} requires every property in \`required\`; missing ${c.map((l) => `\`${l}\``).join(", ")} (emulate optional with a \`null\` union)`
|
|
302
303
|
});
|
|
303
304
|
}
|
|
304
|
-
|
|
305
|
+
n && !t.allowRootAnyOf && "anyOf" in e && r.push({
|
|
305
306
|
path: i,
|
|
306
307
|
keyword: "anyOf",
|
|
307
308
|
reason: `${t.label} forbids \`anyOf\` at the ROOT ("Root level object of a schema must be an object, and not use anyOf")`
|
|
308
309
|
}), Object.keys(e).filter((p) => {
|
|
309
310
|
var c;
|
|
310
311
|
return ((c = t.keywords[p]) == null ? void 0 : c.status) !== "annotation";
|
|
311
|
-
}).length === 0 &&
|
|
312
|
+
}).length === 0 && r.push({
|
|
312
313
|
path: i,
|
|
313
314
|
keyword: "type",
|
|
314
315
|
reason: `unconstrained subschema (annotations only); ${t.label} compiles the schema to a grammar and has no way to express "any JSON". Give it a \`type\``
|
|
315
316
|
});
|
|
316
|
-
const
|
|
317
|
+
const u = (p) => {
|
|
317
318
|
var c;
|
|
318
319
|
return ((c = t.keywords[p]) == null ? void 0 : c.status) === "unsupported";
|
|
319
320
|
};
|
|
320
|
-
for (const p of
|
|
321
|
+
for (const p of kt) {
|
|
321
322
|
const c = e[p];
|
|
322
|
-
if (!(!O(c) ||
|
|
323
|
-
for (const [
|
|
324
|
-
x(h, t, p === "properties" ?
|
|
323
|
+
if (!(!O(c) || u(p)))
|
|
324
|
+
for (const [l, h] of Object.entries(c))
|
|
325
|
+
x(h, t, p === "properties" ? jt(i, l) : `${i}/${p}[${l}]`, false, r);
|
|
325
326
|
}
|
|
326
|
-
for (const p of
|
|
327
|
-
if (!(p in e) ||
|
|
327
|
+
for (const p of xt) {
|
|
328
|
+
if (!(p in e) || u(p)) continue;
|
|
328
329
|
const c = e[p];
|
|
329
|
-
typeof c != "boolean" && x(c, t, p === "items" ? `${i === "(root)" ? "(root)" : i}[]` : `${i}/${p}`, false,
|
|
330
|
+
typeof c != "boolean" && x(c, t, p === "items" ? `${i === "(root)" ? "(root)" : i}[]` : `${i}/${p}`, false, r);
|
|
330
331
|
}
|
|
331
|
-
O(e.additionalProperties) && x(e.additionalProperties, t, `${i}/additionalProperties`, false,
|
|
332
|
-
for (const p of
|
|
332
|
+
O(e.additionalProperties) && x(e.additionalProperties, t, `${i}/additionalProperties`, false, r);
|
|
333
|
+
for (const p of Ot) {
|
|
333
334
|
const c = e[p];
|
|
334
|
-
!Array.isArray(c) ||
|
|
335
|
+
!Array.isArray(c) || u(p) || c.forEach((l, h) => x(l, t, `${i}/${p}[${h}]`, false, r));
|
|
335
336
|
}
|
|
336
337
|
}
|
|
337
|
-
const
|
|
338
|
+
const At = Object.freeze({
|
|
338
339
|
confirm: "Show the user an approval card and wait for their decision before you act. Use this whenever you are about to do something consequential, destructive or irreversible.",
|
|
339
340
|
choice: "Ask the user to pick exactly one option from a list of rich choices. Use this instead of asking in prose when the options are known and finite.",
|
|
340
341
|
tasks: "Show the user a checklist they can tick, either to select several items for you to act on or to track progress through a sequence of steps.",
|
|
@@ -343,125 +344,218 @@ const kt = Object.freeze({
|
|
|
343
344
|
embed: "Embed a video player (YouTube, Vimeo, or a direct player URL) inline in the conversation.",
|
|
344
345
|
artifact: "Show something you built (a page, an app, a set of files) in a framed preview with its source alongside, so the user can look at both."
|
|
345
346
|
});
|
|
346
|
-
class
|
|
347
|
-
constructor(i,
|
|
348
|
-
super(
|
|
347
|
+
class qt extends Error {
|
|
348
|
+
constructor(i, n) {
|
|
349
|
+
super(It(i, n));
|
|
349
350
|
w(this, "name", "UnsupportedCardToolSchemaError");
|
|
350
351
|
w(this, "provider");
|
|
351
352
|
w(this, "source");
|
|
352
353
|
w(this, "cards");
|
|
353
|
-
this.provider = i.id, this.source = i.source, this.cards =
|
|
354
|
+
this.provider = i.id, this.source = i.source, this.cards = n;
|
|
354
355
|
}
|
|
355
356
|
/** Every distinct keyword named across every failing card, deduped, in order. */
|
|
356
357
|
get keywords() {
|
|
357
358
|
const i = /* @__PURE__ */ new Set();
|
|
358
|
-
for (const
|
|
359
|
+
for (const n of this.cards) for (const r of n.violations) i.add(r.keyword);
|
|
359
360
|
return [...i];
|
|
360
361
|
}
|
|
361
362
|
}
|
|
362
|
-
function
|
|
363
|
-
const i = [],
|
|
364
|
-
i.push(`${t.length} ${
|
|
365
|
-
for (const
|
|
366
|
-
i.push(""), i.push(` ${
|
|
367
|
-
for (const
|
|
368
|
-
i.push(` ${
|
|
363
|
+
function It(e, t) {
|
|
364
|
+
const i = [], n = t.length === 1 ? "card type" : "card types";
|
|
365
|
+
i.push(`${t.length} ${n} cannot be a STRICT tool definition under ${e.label}:`);
|
|
366
|
+
for (const r of t) {
|
|
367
|
+
i.push(""), i.push(` ${r.toolName} (card type "${r.cardType}") — ${r.violations.length} problem(s):`);
|
|
368
|
+
for (const o of r.violations)
|
|
369
|
+
i.push(` ${o.path}: \`${o.keyword}\` — ${o.reason}`);
|
|
369
370
|
}
|
|
370
371
|
return i.push(""), i.push(` subset source: ${e.source} (read ${e.checkedOn})`), i.push(
|
|
371
|
-
" Fix: drop `strict: true`. Non-strict
|
|
372
|
+
" Fix: drop `strict: true`. Non-strict mode projects a provider-valid schema (root combinators are relaxed with the constraint restated in the description and still enforced by card validation). Rewriting a built-in card schema to fit a strict subset would change what a valid card IS, which is a card-contract change, not a projection setting."
|
|
372
373
|
), i.join(`
|
|
373
374
|
`);
|
|
374
375
|
}
|
|
375
|
-
const
|
|
376
|
-
|
|
376
|
+
const f = (e) => typeof e == "object" && e !== null && !Array.isArray(e), Lt = {
|
|
377
|
+
artifact: "Provide `src` (a preview URL) or `files` — at least one. An envelope with neither is rejected.",
|
|
378
|
+
embed: "For provider 'generic', include `url`. For 'youtube'/'vimeo', include `id` or `url`."
|
|
379
|
+
}, Et = "The root constraint of this schema was relaxed for this provider; consult the card registry for what a valid payload is.", Tt = ["anyOf", "allOf", "oneOf", "not", "enum", "const"];
|
|
380
|
+
function T(e) {
|
|
381
|
+
if (!f(e)) return null;
|
|
382
|
+
const t = {};
|
|
383
|
+
for (const [i, n] of Object.entries(e)) t[i] = { const: n };
|
|
384
|
+
return { type: "object", properties: t, required: Object.keys(e) };
|
|
385
|
+
}
|
|
386
|
+
function Ct(e, t) {
|
|
387
|
+
return e === "anyOf" || e === "oneOf" || e === "allOf" ? Array.isArray(t) ? t.filter(f) : [] : e === "const" ? [T(t)].filter((i) => i !== null) : e === "enum" ? Array.isArray(t) ? t.map(T).filter((i) => i !== null) : [] : [];
|
|
388
|
+
}
|
|
389
|
+
function Rt(e, t) {
|
|
390
|
+
const i = f(e.properties) ? { ...e.properties } : {}, n = new Set(
|
|
391
|
+
Array.isArray(e.required) ? e.required.map(String) : []
|
|
392
|
+
);
|
|
393
|
+
for (const o of t) {
|
|
394
|
+
const a = Ct(o, e[o]);
|
|
395
|
+
if (a.length === 0) continue;
|
|
396
|
+
for (const u of a)
|
|
397
|
+
if (f(u.properties))
|
|
398
|
+
for (const [p, c] of Object.entries(u.properties))
|
|
399
|
+
p in i || (i[p] = c);
|
|
400
|
+
const s = a.map(
|
|
401
|
+
(u) => new Set(Array.isArray(u.required) ? u.required.map(String) : [])
|
|
402
|
+
);
|
|
403
|
+
if (o === "allOf")
|
|
404
|
+
for (const u of s) for (const p of u) n.add(p);
|
|
405
|
+
else {
|
|
406
|
+
const [u, ...p] = s;
|
|
407
|
+
for (const c of u)
|
|
408
|
+
p.every((l) => l.has(c)) && n.add(c);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
Object.keys(i).length > 0 && (e.properties = i);
|
|
412
|
+
const r = [...n].filter((o) => o in i);
|
|
413
|
+
r.length > 0 && (e.required = r);
|
|
414
|
+
}
|
|
415
|
+
function Nt(e, t) {
|
|
416
|
+
const i = e.type;
|
|
417
|
+
if (i !== void 0 && i !== "object")
|
|
418
|
+
throw new TypeError(
|
|
419
|
+
`cardTools: the '${t}' card schema declares root type '${String(i)}', but a provider tool schema root must be an object (OpenAI and Anthropic reject anything else with HTTP 400). Wrap the payload in an object, or project this card with provider 'jsonschema'.`
|
|
420
|
+
);
|
|
421
|
+
e.type = "object";
|
|
422
|
+
}
|
|
423
|
+
function Pt(e, t) {
|
|
424
|
+
const i = Lt[t], n = Tt.filter((r) => r in e);
|
|
425
|
+
if (Nt(e, t), n.length !== 0) {
|
|
426
|
+
Rt(e, n);
|
|
427
|
+
for (const r of n) delete e[r];
|
|
428
|
+
return console.warn(
|
|
429
|
+
`[kai-card-tools] relaxed root ${n.join("/")} on the projected '${R(t)}' tool schema for this provider (it would be refused with HTTP 400); the constraint now lives in the description and is still enforced by card validation.`
|
|
430
|
+
), i ?? Et;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function _t(e, t, i) {
|
|
434
|
+
for (const n of i) {
|
|
435
|
+
let r = e;
|
|
436
|
+
if (n.path !== "")
|
|
437
|
+
for (const a of n.path.split(".")) {
|
|
438
|
+
const s = f(r.properties) ? r.properties : void 0, u = s == null ? void 0 : s[a];
|
|
439
|
+
if (!f(u))
|
|
440
|
+
throw new TypeError(
|
|
441
|
+
`cardTools: require on '${t}': path '${n.path}' does not resolve in the schema`
|
|
442
|
+
);
|
|
443
|
+
r = u;
|
|
444
|
+
}
|
|
445
|
+
const o = r.type === "array" || r.type === void 0 && "items" in r;
|
|
446
|
+
if (o && n.minItems !== void 0 && (r.minItems = n.minItems), !o && n.minItems !== void 0)
|
|
447
|
+
throw new TypeError(
|
|
448
|
+
`cardTools: minItems at require path '${n.path}' applies to array nodes only, but '${n.path}' in the '${t}' schema is not one`
|
|
449
|
+
);
|
|
450
|
+
if (n.required !== void 0) {
|
|
451
|
+
const a = o && f(r.items) ? r.items : r, s = f(a.properties) ? a.properties : void 0;
|
|
452
|
+
for (const p of n.required)
|
|
453
|
+
if (!s || !(p in s))
|
|
454
|
+
throw new TypeError(
|
|
455
|
+
`cardTools: require on '${t}' at path '${n.path}': '${p}' is not a property of that node — a required name with no property would make the tool unsatisfiable`
|
|
456
|
+
);
|
|
457
|
+
const u = Array.isArray(a.required) ? a.required.map(String) : [];
|
|
458
|
+
a.required = [.../* @__PURE__ */ new Set([...u, ...n.required])];
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function Dt(e) {
|
|
377
463
|
return e === "$schema" || e === "$id" || e.startsWith("x-");
|
|
378
464
|
}
|
|
379
|
-
const
|
|
465
|
+
const zt = ["items", "if", "then", "else", "not", "contains", "propertyNames", "unevaluatedItems", "additionalItems"], Mt = ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"], Ut = ["allOf", "anyOf", "oneOf", "prefixItems"];
|
|
380
466
|
function k(e, t) {
|
|
381
|
-
if (!
|
|
467
|
+
if (!f(e)) return e;
|
|
382
468
|
const i = {};
|
|
383
|
-
for (const [
|
|
384
|
-
|
|
385
|
-
for (const
|
|
386
|
-
const
|
|
387
|
-
if (!
|
|
388
|
-
const
|
|
389
|
-
for (const [
|
|
390
|
-
i[
|
|
391
|
-
}
|
|
392
|
-
for (const
|
|
393
|
-
!(
|
|
394
|
-
|
|
395
|
-
for (const
|
|
396
|
-
const
|
|
397
|
-
Array.isArray(
|
|
398
|
-
}
|
|
399
|
-
const
|
|
400
|
-
if (
|
|
401
|
-
const
|
|
402
|
-
for (const
|
|
403
|
-
|
|
404
|
-
i.required =
|
|
469
|
+
for (const [o, a] of Object.entries(e))
|
|
470
|
+
Dt(o) || (i[o] = a);
|
|
471
|
+
for (const o of Mt) {
|
|
472
|
+
const a = i[o];
|
|
473
|
+
if (!f(a)) continue;
|
|
474
|
+
const s = {};
|
|
475
|
+
for (const [u, p] of Object.entries(a)) s[u] = k(p, t);
|
|
476
|
+
i[o] = s;
|
|
477
|
+
}
|
|
478
|
+
for (const o of zt)
|
|
479
|
+
!(o in i) || typeof i[o] == "boolean" || (i[o] = k(i[o], t));
|
|
480
|
+
f(i.additionalProperties) && (i.additionalProperties = k(i.additionalProperties, t));
|
|
481
|
+
for (const o of Ut) {
|
|
482
|
+
const a = i[o];
|
|
483
|
+
Array.isArray(a) && (i[o] = a.map((s) => k(s, t)));
|
|
484
|
+
}
|
|
485
|
+
const n = f(i.properties) ? i.properties : void 0, r = i.type === "object" || n !== void 0;
|
|
486
|
+
if (r && t.closeObjects && !("additionalProperties" in i) && (i.additionalProperties = false), r && t.requireEveryProperty && n) {
|
|
487
|
+
const o = Array.isArray(i.required) ? i.required.map(String) : [], a = Object.keys(n);
|
|
488
|
+
for (const s of a)
|
|
489
|
+
o.includes(s) || (n[s] = Ft(n[s]));
|
|
490
|
+
i.required = a;
|
|
405
491
|
}
|
|
406
492
|
return i;
|
|
407
493
|
}
|
|
408
|
-
function
|
|
409
|
-
if (!
|
|
494
|
+
function Ft(e) {
|
|
495
|
+
if (!f(e)) return e;
|
|
410
496
|
const t = { ...e };
|
|
411
497
|
if ("type" in t) {
|
|
412
498
|
const i = Array.isArray(t.type) ? t.type.map(String) : [String(t.type)];
|
|
413
499
|
i.includes("null") || (t.type = [...i, "null"]);
|
|
414
500
|
} else if (Array.isArray(t.anyOf))
|
|
415
|
-
t.anyOf.some((i) =>
|
|
501
|
+
t.anyOf.some((i) => f(i) && i.type === "null") || (t.anyOf = [...t.anyOf, { type: "null" }]);
|
|
416
502
|
else
|
|
417
503
|
return t;
|
|
418
504
|
return Array.isArray(t.enum) && !t.enum.includes(null) && (t.enum = [...t.enum, null]), t;
|
|
419
505
|
}
|
|
420
|
-
function
|
|
506
|
+
function Jt(e) {
|
|
421
507
|
const t = e;
|
|
422
|
-
return
|
|
508
|
+
return f(t.schemas) ? { schemas: t.schemas, descriptions: t.descriptions ?? {} } : { schemas: e, descriptions: {} };
|
|
423
509
|
}
|
|
424
|
-
function
|
|
425
|
-
const
|
|
426
|
-
if (typeof
|
|
427
|
-
const
|
|
428
|
-
if (typeof
|
|
429
|
-
const
|
|
430
|
-
return typeof
|
|
510
|
+
function Bt(e, t, i) {
|
|
511
|
+
const n = i[e];
|
|
512
|
+
if (typeof n == "string" && n.length > 0) return n;
|
|
513
|
+
const r = At[e];
|
|
514
|
+
if (typeof r == "string" && r.length > 0) return r;
|
|
515
|
+
const o = t.description;
|
|
516
|
+
return typeof o == "string" && o.length > 0 ? o : `Render a \`${e}\` card.`;
|
|
431
517
|
}
|
|
432
518
|
function v(e, t) {
|
|
433
|
-
|
|
434
|
-
|
|
519
|
+
var l;
|
|
520
|
+
const i = t === void 0, n = i ? e : t;
|
|
521
|
+
if (!f(n) || typeof n.provider != "string")
|
|
435
522
|
throw new TypeError('cardTools: an options object with a `provider` is required, e.g. cardTools({ provider: "openai" })');
|
|
436
|
-
const { schemas:
|
|
437
|
-
i ?
|
|
438
|
-
),
|
|
439
|
-
if (
|
|
523
|
+
const { schemas: r, descriptions: o } = Jt(
|
|
524
|
+
i ? Yt() : e
|
|
525
|
+
), a = n.strict === true;
|
|
526
|
+
if (n.provider === "jsonschema" && a)
|
|
440
527
|
throw new TypeError(
|
|
441
528
|
'cardTools: `strict: true` is provider-specific and cannot be checked for `provider: "jsonschema"`, because the two documented strict subsets differ (see @kitn.ai/ui/schemas provider-subsets). Pass provider "openai" or "anthropic" to project and check strictly, or drop `strict`.'
|
|
442
529
|
);
|
|
443
|
-
const
|
|
444
|
-
closeObjects:
|
|
445
|
-
requireEveryProperty:
|
|
530
|
+
const s = a ? n.provider === "openai" ? P : _ : null, u = {
|
|
531
|
+
closeObjects: s !== null,
|
|
532
|
+
requireEveryProperty: s !== null && s.requireAllPropertiesRequired
|
|
446
533
|
}, p = [], c = [];
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
534
|
+
if (n.require !== void 0) {
|
|
535
|
+
for (const h of Object.keys(n.require))
|
|
536
|
+
if (!(h in r))
|
|
537
|
+
throw new TypeError(
|
|
538
|
+
`cardTools: require names card type '${h}', which this call does not carry (carries: ${Object.keys(r).join(", ") || "none"})`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
for (const [h, $] of Object.entries(r)) {
|
|
542
|
+
const g = R(h), b = Bt(h, $, o), d = k($, u), m = s === null && n.provider !== "jsonschema" ? Pt(d, h) : void 0, I = m ? `${b} ${m}` : b, L = (l = n.require) == null ? void 0 : l[h];
|
|
543
|
+
if (L !== void 0 && _t(d, h, L), s) {
|
|
544
|
+
const E = St(d, s);
|
|
545
|
+
if (E.length > 0) {
|
|
546
|
+
c.push({ cardType: h, toolName: g, violations: E });
|
|
453
547
|
continue;
|
|
454
548
|
}
|
|
455
549
|
}
|
|
456
|
-
|
|
550
|
+
n.provider === "openai" ? p.push({ type: "function", function: { name: g, description: I, parameters: d, ...a ? { strict: true } : {} } }) : n.provider === "anthropic" ? p.push({ name: g, description: I, input_schema: d, ...a ? { strict: true } : {} }) : p.push({ name: g, description: b, schema: d });
|
|
457
551
|
}
|
|
458
|
-
if (
|
|
552
|
+
if (s && c.length > 0) throw new qt(s, c);
|
|
459
553
|
return p;
|
|
460
554
|
}
|
|
461
|
-
function
|
|
555
|
+
function Yt() {
|
|
462
556
|
return j;
|
|
463
557
|
}
|
|
464
|
-
const
|
|
558
|
+
const li = {
|
|
465
559
|
form: "kai-form",
|
|
466
560
|
confirm: "kai-confirm",
|
|
467
561
|
tasks: "kai-tasks",
|
|
@@ -470,27 +564,28 @@ const ei = {
|
|
|
470
564
|
embed: "kai-embed",
|
|
471
565
|
artifact: "kai-artifact"
|
|
472
566
|
}, y = (e) => e, j = Object.freeze({
|
|
473
|
-
artifact: y(
|
|
474
|
-
choice: y(
|
|
475
|
-
confirm: y(
|
|
476
|
-
embed: y(
|
|
477
|
-
form: y(
|
|
478
|
-
link: y(
|
|
479
|
-
tasks: y(
|
|
480
|
-
}),
|
|
567
|
+
artifact: y(W),
|
|
568
|
+
choice: y(be),
|
|
569
|
+
confirm: y(Se),
|
|
570
|
+
embed: y(Pe),
|
|
571
|
+
form: y(Ge),
|
|
572
|
+
link: y(rt),
|
|
573
|
+
tasks: y(wt)
|
|
574
|
+
}), hi = Object.freeze(
|
|
481
575
|
Object.keys(j)
|
|
482
576
|
);
|
|
483
|
-
function
|
|
577
|
+
function ii(e) {
|
|
484
578
|
return Object.prototype.hasOwnProperty.call(j, e);
|
|
485
579
|
}
|
|
486
|
-
const
|
|
580
|
+
const tags = { "kai-conversations": "conversation-list", "kai-conversation-item": "conversation-item", "kai-prompt-input": "prompt-input", "kai-chat": "chat", "kai-workspace": "chat-workspace", "kai-thread": "thread", "kai-thinking-bar": "thinking-bar", "kai-model-switcher": "model-switcher", "kai-attachments": "attachments", "kai-message": "message", "kai-markdown": "markdown", "kai-code-block": "code-block", "kai-reasoning": "reasoning", "kai-tool": "tool", "kai-context": "context-meter", "kai-feedback-bar": "feedback-bar", "kai-scope-picker": "chat-scope-picker", "kai-suggestions": "prompt-suggestions", "kai-file-upload": "file-upload", "kai-voice-input": "voice-input", "kai-audio-visualizer": "audio-visualizer", "kai-loader": "loader", "kai-text-shimmer": "text-shimmer", "kai-image": "image", "kai-checkpoint": "checkpoint", "kai-skills": "message-skills", "kai-source": "source", "kai-sources": "source", "kai-response-stream": "response-stream", "kai-empty": "empty", "kai-status": "status", "kai-nav": "nav", "kai-progress-bar": "progress-bar", "kai-coachmark": "coachmark", "kai-tabs": "tabs", "kai-voice-output": "voice-output", "kai-screen": "screen", "kai-chain-of-thought": "chain-of-thought", "kai-resizable": "resizable", "kai-resizable-item": "resizable", "kai-file-tree": "file-tree", "kai-artifact": "artifact", "kai-scroll-button": "scroll-button", "kai-popover": "popover", "kai-switch": "switch", "kai-button": "button", "kai-avatar": "avatar", "kai-badge": "badge", "kai-tooltip": "tooltip", "kai-notice": "notice", "kai-icon": "icon", "kai-separator": "separator", "kai-scroll-area": "scroll-area", "kai-hover-card": "hover-card", "kai-skeleton": "skeleton", "kai-toast-region": "toast", "kai-card": "card", "kai-form": "form", "kai-link-preview": "link-preview", "kai-embed": "embed", "kai-confirm": "confirm-card", "kai-tasks": "tasks", "kai-choice": "choice", "kai-cards": "cards", "kai-compare": "compare", "kai-composer": "composer", "kai-menu": "menu", "kai-dropdown": "dropdown", "kai-command": "command", "kai-prompt-dock": "prompt-dock", "kai-segmented": "segmented", "kai-settings-group": "settings-group", "kai-setting-item": "setting-item", "kai-pane": "pane", "kai-pane-group": "pane-group", "kai-agent-card": "agent-card", "kai-dialog": "dialog", "kai-dock": "dock", "kai-input": "input", "kai-search": "search", "kai-kbd": "kbd", "kai-editable-label": "editable-label" };
|
|
581
|
+
const PACKAGE_NAME$1 = "@kitn.ai/ui";
|
|
487
582
|
const MANIFEST_FILE = "custom-elements.json";
|
|
488
583
|
const SOURCE_TO_PACKAGE_ROOT = ["..", "..", ".."];
|
|
489
584
|
function isThisPackage(root) {
|
|
490
585
|
const manifest = join(root, "package.json");
|
|
491
586
|
if (!existsSync(manifest)) return false;
|
|
492
587
|
try {
|
|
493
|
-
return JSON.parse(readFileSync(manifest, "utf-8")).name === PACKAGE_NAME;
|
|
588
|
+
return JSON.parse(readFileSync(manifest, "utf-8")).name === PACKAGE_NAME$1;
|
|
494
589
|
} catch {
|
|
495
590
|
return false;
|
|
496
591
|
}
|
|
@@ -502,14 +597,14 @@ function resolveManifestPath(fromDir = dirname(fileURLToPath(import.meta.url)))
|
|
|
502
597
|
const expected = join(packageRoot, "dist", MANIFEST_FILE);
|
|
503
598
|
if (!isThisPackage(packageRoot)) {
|
|
504
599
|
throw new Error(
|
|
505
|
-
`[${PACKAGE_NAME}] Cannot locate the Custom Elements Manifest: ${packageRoot} is not the ${PACKAGE_NAME} package root, so ${expected} would not be this package's manifest even if it existed.
|
|
600
|
+
`[${PACKAGE_NAME$1}] Cannot locate the Custom Elements Manifest: ${packageRoot} is not the ${PACKAGE_NAME$1} package root, so ${expected} would not be this package's manifest even if it existed.
|
|
506
601
|
Resolved from: ${fromDir}
|
|
507
602
|
This module must live at <package>/src/agent-tooling/mcp/ (or be bundled beside ${MANIFEST_FILE} in dist/). Resolution deliberately does NOT search parent directories — finding some other checkout's manifest is worse than failing.`
|
|
508
603
|
);
|
|
509
604
|
}
|
|
510
605
|
if (!existsSync(expected)) {
|
|
511
606
|
throw new Error(
|
|
512
|
-
`[${PACKAGE_NAME}] Missing build artifact: ${expected}
|
|
607
|
+
`[${PACKAGE_NAME$1}] Missing build artifact: ${expected}
|
|
513
608
|
Resolved from: ${fromDir}
|
|
514
609
|
The Custom Elements Manifest is generated by the build. Run \`nx build ui\` (or \`npm run build:api\` in packages/ui) and try again.
|
|
515
610
|
Resolution deliberately does NOT search parent directories: binding to a neighbouring checkout's manifest would make this succeed against stale data.`
|
|
@@ -535,17 +630,51 @@ function getElement(tag) {
|
|
|
535
630
|
function listElements() {
|
|
536
631
|
return getDeclarations().filter((d) => d.tagName).map((d) => d.tagName).sort();
|
|
537
632
|
}
|
|
633
|
+
function entryForTag(tag) {
|
|
634
|
+
return tags[tag];
|
|
635
|
+
}
|
|
636
|
+
function optInEntryForTag(tag) {
|
|
637
|
+
if (entryForTag(tag)) return void 0;
|
|
638
|
+
return optInEntries().get(tag);
|
|
639
|
+
}
|
|
640
|
+
const NON_ELEMENT_ENTRIES = /* @__PURE__ */ new Set(["index", "autoloader"]);
|
|
641
|
+
let _optInEntries;
|
|
642
|
+
function optInEntries() {
|
|
643
|
+
if (_optInEntries) return _optInEntries;
|
|
644
|
+
_optInEntries = /* @__PURE__ */ new Map();
|
|
645
|
+
const dir = join(dirname(resolveManifestPath()), "elements");
|
|
646
|
+
let candidates;
|
|
647
|
+
try {
|
|
648
|
+
const registered = new Set(Object.values(tags));
|
|
649
|
+
candidates = readdirSync(dir).filter((f2) => f2.endsWith(".js")).map((f2) => f2.slice(0, -".js".length)).filter((base) => !registered.has(base) && !NON_ELEMENT_ENTRIES.has(base));
|
|
650
|
+
} catch {
|
|
651
|
+
return _optInEntries;
|
|
652
|
+
}
|
|
653
|
+
const unclaimed = listElements().filter((t) => !entryForTag(t));
|
|
654
|
+
for (const base of candidates) {
|
|
655
|
+
let code;
|
|
656
|
+
try {
|
|
657
|
+
code = readFileSync(join(dir, `${base}.js`), "utf-8");
|
|
658
|
+
} catch {
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
for (const tag of unclaimed) {
|
|
662
|
+
if (!_optInEntries.has(tag) && code.includes(`"${tag}"`)) _optInEntries.set(tag, base);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return _optInEntries;
|
|
666
|
+
}
|
|
538
667
|
let _cardTags;
|
|
539
668
|
let _cardHosts;
|
|
540
669
|
function cardTagForType(type) {
|
|
541
670
|
if (!_cardTags) {
|
|
542
671
|
const present = new Set(listElements());
|
|
543
|
-
_cardTags = new Map(Object.entries(
|
|
672
|
+
_cardTags = new Map(Object.entries(li).filter(([, tag]) => present.has(tag)));
|
|
544
673
|
}
|
|
545
674
|
return _cardTags.get(type);
|
|
546
675
|
}
|
|
547
676
|
function cardTypeForTag(tag) {
|
|
548
|
-
for (const name of
|
|
677
|
+
for (const name of hi) {
|
|
549
678
|
if (cardTagForType(name) === tag) return name;
|
|
550
679
|
}
|
|
551
680
|
return void 0;
|
|
@@ -560,6 +689,383 @@ function cardHostTags() {
|
|
|
560
689
|
}
|
|
561
690
|
return _cardHosts;
|
|
562
691
|
}
|
|
692
|
+
const invariants = [
|
|
693
|
+
{
|
|
694
|
+
id: "reactivity-two-halves",
|
|
695
|
+
statement: "A new array reference NOTIFIES; a new object for each changed item makes the change VISIBLE. Editing an existing item needs both. Adds and removes need only the fresh array. Setting the same array back is a no-op even if an item inside it was swapped. The test pins how the KIT behaves — it will render stale unless both arrive — but nothing checks CONSUMER code, so this is a rule you apply, not a guarantee you will be warned about. Reorders follow the same rule as adds and removes; the test names reorders in its title but exercises only an add and a remove, so treat that half as reasoned rather than pinned.",
|
|
696
|
+
appliesTo: { tags: ["kai-chat", "kai-conversations"] },
|
|
697
|
+
enforcedBy: { kind: "test", paths: ["packages/ui/src/components/reactivity-contract.test.tsx"] },
|
|
698
|
+
status: "enforced",
|
|
699
|
+
diagnosis: [
|
|
700
|
+
{
|
|
701
|
+
symptom: "messages render once but never update while streaming",
|
|
702
|
+
cause: "the same array reference is being set back; the element is never notified"
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
symptom: "the list re-renders but an edited item shows stale content",
|
|
706
|
+
cause: "the array is new but the item object identity is unchanged; the reference-keyed <For> keeps the old row"
|
|
707
|
+
}
|
|
708
|
+
],
|
|
709
|
+
examples: [
|
|
710
|
+
{
|
|
711
|
+
wrong: "chat.messages.push({ id, role: 'user', parts: [{ type: 'text', text }] });",
|
|
712
|
+
right: "chat.messages = [...chat.messages, { id, role: 'user', parts: [{ type: 'text', text }] }];",
|
|
713
|
+
note: "Mutating in place never notifies. Neither does assigning the same array reference back — the setter compares references."
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
wrong: "messages[last].parts.push(part);",
|
|
717
|
+
right: "chat.messages = messages.map((m, i) => (i === last ? { ...m, parts: [...m.parts, part] } : m));",
|
|
718
|
+
note: "This is the half that gets missed. A fresh array alone notifies, but the reference-keyed <For> keeps the old row until the EDITED ITEM is a new object too. createAssistantStream from @kitn.ai/ui/state already does both."
|
|
719
|
+
}
|
|
720
|
+
]
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
id: "props-not-attributes",
|
|
724
|
+
statement: "Set arrays, objects and functions as JS PROPERTIES on the element instance. Only scalars (strings, numbers, booleans) belong in attributes, and the derived layer's scalar flag records which prop is which. What actually goes wrong, because the mechanism is not the obvious one: a framework template binding or String() stringifies an array to '[object Object]', which is not JSON, and the attribute path falls back to handing the element that raw STRING — so the prop is silently a string and the list renders nothing. A function cannot survive JSON at all: JSON.stringify({ onSubmit }) is '{}', so every callback is dropped. And an attribute re-set is not how updates are delivered — see reactivity-two-halves. A hand-written, valid-JSON attribute does happen to parse today, because a transitive dependency JSON.parses attributes whose declared default is not a string, but that is that dependency's behaviour and not this kit's contract; do not build on it. NOTHING IN THIS REPO ENFORCES THIS — it is a consumer contract, and the scalar flag in the derived layer is how the catalog SERVES the fact, which is not the same as checking it.",
|
|
725
|
+
appliesTo: {},
|
|
726
|
+
// kind:'none' after measurement, replacing a `structural` pointer at
|
|
727
|
+
// define.tsx that did not contain the claimed mechanism. What define.tsx
|
|
728
|
+
// actually does is install non-reflecting accessors for the handful of props
|
|
729
|
+
// colliding with reflected global IDL attributes (role/hidden/autofocus), in
|
|
730
|
+
// the OPPOSITE direction: it stops property writes reflecting TO attributes.
|
|
731
|
+
// The attribute->property path lives in component-register, a transitive
|
|
732
|
+
// dependency, and measured in the real jsdom project against the real
|
|
733
|
+
// element it PARSES a JSON attribute onto the property rather than rejecting
|
|
734
|
+
// it. Nothing here checks consumer code for the contract; S1's scoring
|
|
735
|
+
// exercises a property update on kai-chat.messages, but no scenario names
|
|
736
|
+
// this invariant, so it is not claimed as a measurement either.
|
|
737
|
+
enforcedBy: { kind: "none" },
|
|
738
|
+
status: "open",
|
|
739
|
+
diagnosis: [
|
|
740
|
+
{
|
|
741
|
+
symptom: "an element renders empty and the data looks right in devtools",
|
|
742
|
+
cause: "the property holds the STRING '[object Object]', not an array; it arrived through an attribute that was stringified rather than assigned"
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
symptom: "everything renders but no callback ever fires",
|
|
746
|
+
cause: "the object went through JSON into an attribute, and JSON drops functions silently"
|
|
747
|
+
}
|
|
748
|
+
],
|
|
749
|
+
examples: [
|
|
750
|
+
{
|
|
751
|
+
wrong: "el.setAttribute('messages', String(messages));",
|
|
752
|
+
right: "el.messages = messages;",
|
|
753
|
+
note: "Measured against the real element: the attribute becomes '[object Object]', JSON.parse fails, and the fallback leaves a STRING on the prop. messages is scalar:false in the derived layer; placeholder, loading and theme are scalar:true and do belong in attributes."
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
wrong: "cards.setAttribute('policy', JSON.stringify({ onSubmit }));",
|
|
757
|
+
right: "cards.policy = { onSubmit };",
|
|
758
|
+
note: "Measured: JSON.stringify({ onSubmit }) is '{}', so the handler is gone before the attribute is even set. No attribute can carry a function."
|
|
759
|
+
}
|
|
760
|
+
]
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
id: "events-non-bubbling",
|
|
764
|
+
statement: "Non-bubbling is the default: public kai-* events are dispatched through the one helper that hard-codes bubbles:false and composed:false, so listen on the element itself, never on a parent or document. The protocol exceptions (kai-maximize-intent, kai-maximize-state and kai-card) bubble or compose deliberately and are listed in the derived layer under eventExceptions — do not generalise from them to the rest.",
|
|
765
|
+
appliesTo: {},
|
|
766
|
+
enforcedBy: { kind: "structural", path: "packages/ui/src/elements/define.tsx" },
|
|
767
|
+
status: "enforced",
|
|
768
|
+
diagnosis: [
|
|
769
|
+
{
|
|
770
|
+
symptom: "a delegated listener on document or a parent never fires",
|
|
771
|
+
cause: "kai-* events do not bubble; attach the listener to the element that dispatches it"
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
symptom: "a listener on the element works, but the same one on a wrapper div does not",
|
|
775
|
+
cause: "same cause; only the three eventExceptions cross the element boundary"
|
|
776
|
+
}
|
|
777
|
+
],
|
|
778
|
+
examples: [
|
|
779
|
+
{
|
|
780
|
+
wrong: "document.addEventListener('kai-submit', (e) => send(e.detail.value));",
|
|
781
|
+
right: "chat.addEventListener('kai-submit', (e) => send(e.detail.value));",
|
|
782
|
+
note: "The dispatch helper in src/elements/define.tsx passes { bubbles: false, composed: false }, so nothing above the host ever sees the event."
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
wrong: "wrapper.addEventListener('kai-message-action', handleAction);",
|
|
786
|
+
right: "chat.addEventListener('kai-message-action', handleAction);",
|
|
787
|
+
note: "Delegating from a wrapper is the most common shape of this bug, because it is the habit every DOM framework teaches."
|
|
788
|
+
}
|
|
789
|
+
]
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
id: "host-coordinates",
|
|
793
|
+
statement: "There is no store. Data flows in via properties, out via events, and the host wires element A to element B. Solid context does not cross element boundaries, so nothing coordinates elements except the host application, and no element owns another. Placing two elements in the same subtree wires nothing.",
|
|
794
|
+
appliesTo: {},
|
|
795
|
+
enforcedBy: { kind: "none" },
|
|
796
|
+
status: "open",
|
|
797
|
+
diagnosis: [
|
|
798
|
+
{
|
|
799
|
+
symptom: "two elements are expected to sync but do not",
|
|
800
|
+
cause: "nothing auto-coordinates; the host must listen on one element and set properties on the other"
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
symptom: "a property assignment is silently ignored and the prop is not in the reference",
|
|
804
|
+
cause: "the data was put on the element that displays the conversation rather than the one that owns the list"
|
|
805
|
+
}
|
|
806
|
+
],
|
|
807
|
+
examples: [
|
|
808
|
+
{
|
|
809
|
+
wrong: "chat.conversations = rows;",
|
|
810
|
+
right: "conversations.conversations = rows;",
|
|
811
|
+
note: "kai-chat has no conversations prop — the sidebar is its own element. One element never holds the whole app state."
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
wrong: "chat.addEventListener('kai-conversation-select', (e) => load(e.detail.id));",
|
|
815
|
+
right: "conversations.addEventListener('kai-conversation-select', (e) => { chat.messages = threadsById[e.detail.id]; });",
|
|
816
|
+
note: "Event out of A, property into B, wired by the host. The event is dispatched by the element that owns the list, so that is where the listener goes."
|
|
817
|
+
}
|
|
818
|
+
]
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
id: "untrusted-model-output",
|
|
822
|
+
statement: "Everything the model produced is untrusted input: a MessagePart, card envelope or tool argument reaching innerHTML, an href or src, window.open or an iframe is a vulnerability. THE DEFECT IS NEVER A MISSING GUARD, IT IS WHICH PATH GOT IT — every one found so far sat on a path the CONSUMER controls while the model-controlled path beside it had none. So put a policy on the sink, and MATCH THE LIST TO THE SINK rather than reaching for one universal list: http:, https: and mailto: for anything navigable the user may click, resolved against the page so ordinary relative links still work; http: and https: ONLY for a model-supplied citation, which is a reference to a page on the public web and has no business being relative. Those are two lists because there are two sinks — it is the same split the kit makes internally between SAFE_SCHEMES and RENDERABLE_SCHEMES, not a variant invented here. Whichever you use, parse inside a try/catch and RETURN FALSE on an unparseable URL: new URL() throws, and a throw at a sink crashes the render. And render model text as TEXT. Escaping is the correct rendering: the source text must stay VISIBLE as well as inert, because a filter that deleted it would pass the security check and be a worse UI. COVERAGE, and read this before trusting CI here: the three XSS suites are tests and ONLY tests. They run in the required test job, so the vectors they pin cannot come back — but NOTHING structural stops a NEW sink landing unguarded. No lint script in the package is about sinks, and the coupling map's unenforced list has no entry for the class. A new sink is caught in review or not at all.",
|
|
823
|
+
appliesTo: {},
|
|
824
|
+
// WHAT THE THREE SUITES DO NOT CATCH: they pin the vectors that were FOUND
|
|
825
|
+
// (#246 markdown innerHTML, #247 the artifact's three URL sinks, and the
|
|
826
|
+
// hostile-stream path), so they are regression guards, not a guard over the
|
|
827
|
+
// CLASS. A newly written component that puts model text on a fresh
|
|
828
|
+
// unguarded sink adds no failing test anywhere. Verified against
|
|
829
|
+
// HANDOFF-2026-08-13 §13.2 ("They are tests and only tests... nothing
|
|
830
|
+
// structural stops a NEW sink landing unguarded"), and re-checked against
|
|
831
|
+
// the tree: none of packages/ui/scripts/lint-*.mjs concerns sinks, and
|
|
832
|
+
// docs/coupling-map.md has no row for the class. Kept as kind:'test'
|
|
833
|
+
// because the regression coverage is real; status is `partial` because the
|
|
834
|
+
// class is not covered, and the statement carries the gap.
|
|
835
|
+
enforcedBy: {
|
|
836
|
+
kind: "test",
|
|
837
|
+
paths: [
|
|
838
|
+
"packages/ui/tests/components/markdown-xss.test.tsx",
|
|
839
|
+
"packages/ui/tests/components/artifact-url-xss.test.tsx",
|
|
840
|
+
"packages/ui/tests/components/hostile-model-output.test.tsx"
|
|
841
|
+
]
|
|
842
|
+
},
|
|
843
|
+
status: "partial",
|
|
844
|
+
diagnosis: [
|
|
845
|
+
{
|
|
846
|
+
symptom: "a custom renderer for a tool result or a card body executes markup the model emitted",
|
|
847
|
+
cause: "model text reached innerHTML on a hand-written path; the guarded path is the one every real message flows through, and this one bypassed it"
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
symptom: "a citation or card link navigates to javascript: or data:",
|
|
851
|
+
cause: "a model-supplied URL reached an href, a src or window.open with no scheme check"
|
|
852
|
+
}
|
|
853
|
+
],
|
|
854
|
+
examples: [
|
|
855
|
+
{
|
|
856
|
+
wrong: "el.innerHTML = part.text;",
|
|
857
|
+
right: "el.textContent = part.text;",
|
|
858
|
+
note: "For rich text render the part through <kai-markdown>, which escapes rather than sanitizes (src/components/markdown.tsx) and filters link and image URLs. Never hand-roll a second markdown-to-innerHTML path."
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
wrong: "window.open(card.url, '_blank');",
|
|
862
|
+
right: "const isNavigable = (u) => { try { return ['http:', 'https:', 'mailto:'].includes(new URL(u, location.href).protocol); } catch { return false; } };\nif (isNavigable(card.url)) window.open(card.url, '_blank', 'noopener,noreferrer');",
|
|
863
|
+
note: "THE try/catch IS NOT OPTIONAL: new URL() THROWS on an unparseable input like 'http://[', and an uncaught throw here crashes the render instead of blocking the link — worse than the bug you are fixing. Returning false is the whole contract. Resolving against location.href is deliberate: a relative or fragment href inherits http: and is allowed, which is what makes ordinary links keep working. REPO-INTERNAL ONLY: contributors inside this package use isSafeUrl/SAFE_SCHEMES from src/primitives/card-routing.ts, which has this exact shape — neither is reachable from the published package, so never emit an import for them."
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
wrong: "<a href={source.url}>{source.title}</a>",
|
|
867
|
+
right: `const isCitationUrl = (u) => { try { return ['http:', 'https:'].includes(new URL(u).protocol); } catch { return false; } };
|
|
868
|
+
{isCitationUrl(source.url) ? <a href={source.url} rel="noopener noreferrer">{source.title}</a> : <span>{source.title}</span>}`,
|
|
869
|
+
note: "No base here, unlike the navigable case: a model-supplied citation is a reference to a page on the public web, so a relative path is not a citation and returns false. It must RETURN false, not throw — a throw escapes the ternary and the fallback never renders, which is exactly the deleted-text failure this invariant's own statement forbids. The <span> keeps the title VISIBLE. REPO-INTERNAL equivalent: isRenderableLink in src/primitives/link-preview.ts, also not exported."
|
|
870
|
+
}
|
|
871
|
+
]
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
id: "kit-parses-consumer-fetches",
|
|
875
|
+
statement: "The kit parses; the consumer fetches. Two halves, covered differently. KIT SIDE: every MessagePart variant the wire encodes must be accounted for, or a variant is gone once the request leaves — that half is enforced by lint:silent-drops in CI. CONSUMER SIDE: never hand-roll an SSE reader; import readOpenAIStream, readAnthropicStream or readModelStream from @kitn.ai/ui/wire, and fetch from your own endpoint, because there is no client, no key handling and no provider SDK below wire/. NO CI CHECK COVERS THAT SECOND HALF — no guard reads consumer or scaffolded code for a hand-rolled reader; it is measured by the acceptance deck instead, at scenario S2, whose scoring line is 'imports readOpenAIStream from @kitn.ai/ui/wire; no hand-rolled SSE reader anywhere in the output'.",
|
|
876
|
+
appliesTo: {},
|
|
877
|
+
// WHAT lint:silent-drops DOES NOT CATCH: it analyzes src/wire, so it covers
|
|
878
|
+
// the kit-side half only. A consumer or a scaffold that hand-rolls its own
|
|
879
|
+
// SSE loop never enters its scan, and the lint stays green. That gap is
|
|
880
|
+
// stated in the statement above so no reader concludes CI catches it, and
|
|
881
|
+
// scenario S2 is what actually measures it. Recorded rather than closed:
|
|
882
|
+
// flipping this to kind:'none' would discard a real guard over a real half,
|
|
883
|
+
// which is what `status: 'partial'` exists to express.
|
|
884
|
+
enforcedBy: { kind: "lint", script: "lint:silent-drops" },
|
|
885
|
+
status: "partial",
|
|
886
|
+
diagnosis: [
|
|
887
|
+
{
|
|
888
|
+
symptom: "streaming works for one provider and silently drops parts for another",
|
|
889
|
+
cause: "a hand-rolled reader misses part variants the wire layer already handles; replace it with the wire import"
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
symptom: "tokens arrive glued together, or a multibyte character renders as garbage mid-stream",
|
|
893
|
+
cause: "a hand-rolled reader split on a data: prefix and assumed one frame per chunk; keep-alive comments, multi-line frames and codepoints split across a socket boundary are all real"
|
|
894
|
+
}
|
|
895
|
+
],
|
|
896
|
+
examples: [
|
|
897
|
+
{
|
|
898
|
+
wrong: "text += JSON.parse(line.replace('data: ', '')).choices[0].delta.content;",
|
|
899
|
+
right: "const turn = await readOpenAIStream(res, stream);",
|
|
900
|
+
note: "import { readOpenAIStream } from '@kitn.ai/ui/wire'; createAssistantStream from '@kitn.ai/ui/state' owns the message, the reader fills it. readAnthropicStream and readModelStream are the other two entry points."
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
wrong: "await fetch('https://api.openai.com/v1/chat/completions', { headers: { Authorization: 'Bearer ' + apiKey } });",
|
|
904
|
+
right: "await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: toOpenAIMessages(history) }) });",
|
|
905
|
+
note: "The consumer fetches, from their own endpoint. A provider key in the browser is a leaked key; toOpenAIMessages/toAnthropicMessages encode the thread for the wire."
|
|
906
|
+
}
|
|
907
|
+
]
|
|
908
|
+
},
|
|
909
|
+
{
|
|
910
|
+
id: "upgrade-race",
|
|
911
|
+
statement: "A property set before the element upgrades is lost. On script-tag targets, load order is not ours. Until issue #99 option B (upgrade-property preservation in defineWebComponent) lands, every script-tag recipe must state this race loudly and set properties only after registration — await customElements.whenDefined(tag), which is the guarantee. A timer, or DOMContentLoaded, is a guess about load order rather than a guarantee: registration can land later, from an async chunk or a dynamically inserted script.",
|
|
912
|
+
appliesTo: { targets: ["script-tag"] },
|
|
913
|
+
enforcedBy: { kind: "none", until: "issue #99 option B lands in defineWebComponent" },
|
|
914
|
+
status: "open",
|
|
915
|
+
diagnosis: [
|
|
916
|
+
{
|
|
917
|
+
symptom: "properties set in inline script are ignored on a CDN page",
|
|
918
|
+
cause: "the element had not upgraded yet; the set landed on a plain HTMLElement and was lost"
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
symptom: "the same code works under a bundler and not from a script tag",
|
|
922
|
+
cause: "the bundler happened to order the registration first; a script tag gives no such guarantee"
|
|
923
|
+
}
|
|
924
|
+
],
|
|
925
|
+
examples: [
|
|
926
|
+
{
|
|
927
|
+
wrong: "setTimeout(() => { chat.messages = messages; }, 0);",
|
|
928
|
+
right: "customElements.whenDefined('kai-chat').then(() => { chat.messages = messages; });",
|
|
929
|
+
note: "A timer bets on load order. whenDefined resolves when the registry actually has the tag, which is the thing you need to be true."
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
wrong: "document.addEventListener('DOMContentLoaded', () => { chat.messages = messages; });",
|
|
933
|
+
right: "customElements.whenDefined('kai-chat').then(() => { chat.messages = messages; });",
|
|
934
|
+
note: "DOMContentLoaded is about the parser, not the registry. It says the markup is there, never that the element behind the tag has been defined."
|
|
935
|
+
}
|
|
936
|
+
]
|
|
937
|
+
}
|
|
938
|
+
];
|
|
939
|
+
const surfaceRecipes = [
|
|
940
|
+
{
|
|
941
|
+
id: "workspace-chat",
|
|
942
|
+
intent: "Full-screen chat with a conversations sidebar; assistant replies can open artifacts in a resizable side panel.",
|
|
943
|
+
archetypes: ["full-screen"],
|
|
944
|
+
targets: ["bundler"],
|
|
945
|
+
ingredients: ["kai-chat", "kai-conversations", "kai-resizable", "kai-artifact"],
|
|
946
|
+
backend: { endpoint: "consumer-owned", reader: "readModelStream" },
|
|
947
|
+
// WHERE the parts go, which the wiring edges below never said. Until this
|
|
948
|
+
// field existed a builder could read the whole recipe, the whole element
|
|
949
|
+
// reference and the whole scaffold and still not know whether the rail was a
|
|
950
|
+
// child of the chat or a sibling beside it — and the two answers lay out
|
|
951
|
+
// differently, so it is not a detail anyone can defer. One agent building
|
|
952
|
+
// from the MCP said so in as many words and then guessed.
|
|
953
|
+
//
|
|
954
|
+
// Slotted, and the tree decided it rather than taste: `<kai-chat>` declares a
|
|
955
|
+
// `sidebar` slot whose own description is "Left column (your nav /
|
|
956
|
+
// conversation list). Fixed width; use compose-your-own for resizable", and
|
|
957
|
+
// this recipe's own corpus story — chat-slots.stories.tsx — composes exactly
|
|
958
|
+
// `<kai-conversations slot="sidebar">` inside `<kai-chat>`.
|
|
959
|
+
//
|
|
960
|
+
// THE TREE ALSO PUBLISHES THE OTHER ANSWER, and an earlier version of this
|
|
961
|
+
// comment claimed it did not ("nothing in the tree composes them as
|
|
962
|
+
// siblings") — asserted without looking, which is the same defect as the
|
|
963
|
+
// over-generous `note` below it. The docs put the pair side by side in at
|
|
964
|
+
// least five places: guides/frameworks/html.mdx, svelte.mdx (a section
|
|
965
|
+
// titled "Add a conversation sidebar"), angular.mdx, vue.mdx, and
|
|
966
|
+
// examples/knowledge-base.mdx with its live demo. Every one of them is a
|
|
967
|
+
// layout the CONSUMER owns — a flex row they write, or `<kai-resizable>` —
|
|
968
|
+
// which is the compose-your-own path the slot's own description points at,
|
|
969
|
+
// and is what you want when the rail must resize. So the two are not in
|
|
970
|
+
// conflict about what is legal; this record says which one the RECIPE is,
|
|
971
|
+
// and the docs currently present the sibling form without saying that.
|
|
972
|
+
composition: [
|
|
973
|
+
{
|
|
974
|
+
child: "kai-conversations",
|
|
975
|
+
parent: "kai-chat",
|
|
976
|
+
slot: "sidebar",
|
|
977
|
+
// Every clause here is checked against chat-thread.tsx, not inferred
|
|
978
|
+
// from the slot's name. The shell's whole sidebar implementation is one
|
|
979
|
+
// `<aside part="sidebar" class="flex w-64 shrink-0 …">` around a
|
|
980
|
+
// `<slot name="sidebar" />`: fixed width, no responsive class in the
|
|
981
|
+
// file, no collapse logic, and `<kai-chat>` never listens for
|
|
982
|
+
// `kai-collapse-toggle`. Collapse belongs to the rail
|
|
983
|
+
// (`collapsed`/`collapse()`), and conversation-list.tsx says so in as
|
|
984
|
+
// many words: "the host owns the surrounding region". A note that
|
|
985
|
+
// credited the shell with collapse and a breakpoint would send a builder
|
|
986
|
+
// into a 16rem empty column holding a floating reopen button.
|
|
987
|
+
note: `the rail is a light-DOM child of <kai-chat> carrying slot="sidebar", not a sibling: the shell renders it into its own ::part(sidebar) aside, a FIXED-WIDTH column (w-64 in chat-thread.tsx, exposed as a part so you can restyle it) — and that is the whole of what the shell does. Collapse is the RAIL's own (collapsed / collapse() / kai-collapse-toggle) and <kai-chat> does not react to it, so a collapsed rail leaves the column at its fixed width; there is no responsive behaviour here either. Give the rail height (display:block;height:100%) and drive it through its own JS properties — being slotted changes where it renders, never how it is wired. If the COLUMN itself has to collapse, resize or respond to width, that is the <kai-workspace> layout shell (its start aside: startCollapsed / collapseBelow / drawer-below, widths via the --kai-workspace-start-* custom properties) or a layout you own`
|
|
988
|
+
}
|
|
989
|
+
],
|
|
990
|
+
wiring: [
|
|
991
|
+
{
|
|
992
|
+
from: "kai-conversations",
|
|
993
|
+
event: "kai-conversation-select",
|
|
994
|
+
to: "kai-chat",
|
|
995
|
+
property: "messages",
|
|
996
|
+
note: "detail is {id}; the host looks the thread up and assigns it — a new array AND a new object per changed item (reactivity-two-halves)"
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
from: "kai-conversations",
|
|
1000
|
+
event: "kai-new-chat",
|
|
1001
|
+
to: "kai-chat",
|
|
1002
|
+
property: "messages",
|
|
1003
|
+
note: "detail is empty by design; the event is the whole signal and the host resets to an empty thread"
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
from: "kai-chat",
|
|
1007
|
+
event: "kai-submit",
|
|
1008
|
+
to: "kai-chat",
|
|
1009
|
+
property: "messages",
|
|
1010
|
+
note: "host reads event.detail.value, appends the user turn, streams the reply through the wire reader onto parts[]"
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
from: "kai-artifact",
|
|
1014
|
+
event: "kai-maximize-change",
|
|
1015
|
+
to: "kai-resizable",
|
|
1016
|
+
property: "maximizedIndex",
|
|
1017
|
+
note: "detail is {maximized}, NOT an index: the host knows which panel holds the artifact and mirrors the boolean onto that index (null to restore). Only needed when the panel is not an ancestor — an artifact INSIDE a kai-resizable-item already drives it through the bubbling kai-maximize-intent protocol, with no host code at all"
|
|
1018
|
+
}
|
|
1019
|
+
],
|
|
1020
|
+
invariants: [
|
|
1021
|
+
"reactivity-two-halves",
|
|
1022
|
+
"props-not-attributes",
|
|
1023
|
+
"events-non-bubbling",
|
|
1024
|
+
"host-coordinates",
|
|
1025
|
+
"kit-parses-consumer-fetches",
|
|
1026
|
+
"untrusted-model-output"
|
|
1027
|
+
],
|
|
1028
|
+
// No single story composes all four ingredients, so the corpus names what
|
|
1029
|
+
// each half is really proven by rather than one path that only half applies:
|
|
1030
|
+
// chat-slots composes kai-chat with kai-conversations, split-workspace
|
|
1031
|
+
// composes kai-resizable with kai-artifact, and surfaces.test.ts is where
|
|
1032
|
+
// the four wiring edges above are actually executed.
|
|
1033
|
+
corpus: [
|
|
1034
|
+
"packages/ui/src/elements/chat-slots.stories.tsx",
|
|
1035
|
+
"packages/ui/src/elements/split-workspace.stories.tsx",
|
|
1036
|
+
"packages/ui/src/agent-tooling/catalog/surfaces.test.ts"
|
|
1037
|
+
]
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
// The script-tag instance. Without it, DeliveryTarget 'script-tag' and the
|
|
1041
|
+
// whole upgrade-race invariant exist in the schema with no recipe using
|
|
1042
|
+
// them, and S5 has nothing to reconstruct from.
|
|
1043
|
+
id: "support-widget-script-tag",
|
|
1044
|
+
intent: "A docked support chat widget added to a page with a script tag and no build step, talking to an endpoint the site owner already runs. The CMS case.",
|
|
1045
|
+
archetypes: ["widget", "docked"],
|
|
1046
|
+
targets: ["script-tag"],
|
|
1047
|
+
ingredients: ["kai-chat"],
|
|
1048
|
+
backend: { endpoint: "consumer-owned", reader: "readOpenAIStream" },
|
|
1049
|
+
wiring: [
|
|
1050
|
+
{
|
|
1051
|
+
from: "kai-chat",
|
|
1052
|
+
event: "kai-submit",
|
|
1053
|
+
to: "kai-chat",
|
|
1054
|
+
property: "messages",
|
|
1055
|
+
note: "host reads event.detail.value, appends the user turn, streams the reply through the wire reader; on this target the host is an inline script, not a framework, so the listener must be attached after customElements.whenDefined (upgrade-race)"
|
|
1056
|
+
}
|
|
1057
|
+
],
|
|
1058
|
+
invariants: [
|
|
1059
|
+
"upgrade-race",
|
|
1060
|
+
"reactivity-two-halves",
|
|
1061
|
+
"props-not-attributes",
|
|
1062
|
+
"events-non-bubbling",
|
|
1063
|
+
"kit-parses-consumer-fetches",
|
|
1064
|
+
"untrusted-model-output"
|
|
1065
|
+
],
|
|
1066
|
+
corpus: ["packages/ui/README.md"]
|
|
1067
|
+
}
|
|
1068
|
+
];
|
|
563
1069
|
const JS_ONLY_TYPE_PATTERNS = /\[\]|\{|Record</;
|
|
564
1070
|
function isJsOnlyType(typeText) {
|
|
565
1071
|
return typeText ? JS_ONLY_TYPE_PATTERNS.test(typeText) : false;
|
|
@@ -591,7 +1097,7 @@ function loopWiring(example) {
|
|
|
591
1097
|
return [
|
|
592
1098
|
"",
|
|
593
1099
|
"#### Wiring the loop",
|
|
594
|
-
`- **Tool names are \`${S}<card type>\`.** \`${
|
|
1100
|
+
`- **Tool names are \`${S}<card type>\`.** \`${R(example)}\` produces \`{ type: "${example}" }\`. Use \`isCardTool(name)\` to split card calls from your own tools; never match the prefix by hand.`,
|
|
595
1101
|
"- **`tool_call_id` becomes `CardEnvelope.id`, unchanged.** That is what makes a revision UPSERT: a model that re-sends the same tool call id replaces the card in place instead of rendering a second copy of it. Generating your own id breaks that silently.",
|
|
596
1102
|
"- **`cardFromToolCall(name, input, { id: call.id })`** turns the call into a renderable envelope, or returns `null` when it is one of your own tools so the loop falls through to it. It never throws.",
|
|
597
1103
|
`- **Everything above is one import:** \`import { cardTools, cardFromToolCall, isCardTool } from '@kitn.ai/ui/schemas'\`. The raw documents also ship as JSON (\`@kitn.ai/ui/schemas/${example}.schema.json\`) for a Python or Go backend.`
|
|
@@ -603,7 +1109,7 @@ function renderCardContract(tag, cardType, provider) {
|
|
|
603
1109
|
const lines = [
|
|
604
1110
|
"",
|
|
605
1111
|
"### Card contract",
|
|
606
|
-
`\`<${tag}>\` renders \`CardEnvelope.type: "${cardType}"\`. A model does not emit an envelope; it calls the tool \`${
|
|
1112
|
+
`\`<${tag}>\` renders \`CardEnvelope.type: "${cardType}"\`. A model does not emit an envelope; it calls the tool \`${R(cardType)}\`, and the kit turns that call back into one.`
|
|
607
1113
|
];
|
|
608
1114
|
lines.push(
|
|
609
1115
|
"",
|
|
@@ -623,25 +1129,185 @@ function renderCardContract(tag, cardType, provider) {
|
|
|
623
1129
|
return lines;
|
|
624
1130
|
}
|
|
625
1131
|
function renderCardHost(tag) {
|
|
626
|
-
const rows =
|
|
1132
|
+
const rows = hi.map((type) => {
|
|
627
1133
|
const cardTag = cardTagForType(type);
|
|
628
|
-
return `- \`${type}\` → \`<${cardTag ?? "(no element)"}>\`, tool \`${
|
|
1134
|
+
return `- \`${type}\` → \`<${cardTag ?? "(no element)"}>\`, tool \`${R(type)}\``;
|
|
629
1135
|
});
|
|
630
1136
|
return [
|
|
631
1137
|
"",
|
|
632
1138
|
"### Card contract",
|
|
633
1139
|
`\`<${tag}>\` renders generative-UI cards inside the thread. It is a HOST: it draws whatever envelopes arrive, and carries the two props that say what a card may be.`,
|
|
634
1140
|
"",
|
|
635
|
-
`- **\`cardTypes\`** — \`Record<envelope type, custom element tag>\`. What DRAWS a card. Merged over the built-ins, so your entry overrides ours and the other ${
|
|
636
|
-
`- **\`cardSchemas\`** — \`Record<envelope type, JSON Schema>\`. What a VALID card looks like. This is the prop that carries YOUR OWN card schemas into the browser validator; the ${
|
|
1141
|
+
`- **\`cardTypes\`** — \`Record<envelope type, custom element tag>\`. What DRAWS a card. Merged over the built-ins, so your entry overrides ours and the other ${hi.length - 1} still render.`,
|
|
1142
|
+
`- **\`cardSchemas\`** — \`Record<envelope type, JSON Schema>\`. What a VALID card looks like. This is the prop that carries YOUR OWN card schemas into the browser validator; the ${hi.length} built-ins are already known. Both are objects, so both are JS properties (\`el.cardSchemas = …\`), never HTML attributes.`,
|
|
637
1143
|
"",
|
|
638
1144
|
"Write both once with `createCardRegistry({ use, custom })` from `@kitn.ai/ui/schemas` and thread it to both ends: `el.cardTypes = cards.tags` / `el.cardSchemas = cards.validationSchemas` on the client, `cardTools(cards, { provider })` on the route.",
|
|
639
1145
|
"",
|
|
640
1146
|
"Built-in card types (ask `component_reference` for one of these tags to get its schema and tool definition):",
|
|
641
1147
|
...rows,
|
|
642
|
-
...loopWiring(
|
|
1148
|
+
...loopWiring(hi[0])
|
|
643
1149
|
];
|
|
644
1150
|
}
|
|
1151
|
+
function invariantsFor(tag) {
|
|
1152
|
+
return invariants.filter((i) => !i.appliesTo.tags || i.appliesTo.tags.includes(tag));
|
|
1153
|
+
}
|
|
1154
|
+
function compositionLines(r, noteSep) {
|
|
1155
|
+
if (!r.composition) return [];
|
|
1156
|
+
return [
|
|
1157
|
+
"- **Composition** — where the parts go (a slotted child, not a sibling):",
|
|
1158
|
+
...r.composition.map(
|
|
1159
|
+
(c) => ` - \`<${c.child} slot="${c.slot}">\` goes INSIDE \`<${c.parent}>\`` + (noteSep && c.note ? `${noteSep}${c.note}` : "")
|
|
1160
|
+
)
|
|
1161
|
+
];
|
|
1162
|
+
}
|
|
1163
|
+
function recipesFor(tag) {
|
|
1164
|
+
return surfaceRecipes.filter((r) => r.ingredients.includes(tag));
|
|
1165
|
+
}
|
|
1166
|
+
function coverageOf(inv) {
|
|
1167
|
+
const by = inv.enforcedBy;
|
|
1168
|
+
const guard = by.kind === "test" ? `the kit's own tests (${by.paths.join(", ")})` : by.kind === "lint" ? `the kit's \`${by.script}\` script, in required CI` : by.kind === "structural" ? `a structural guarantee in ${by.path}` : "";
|
|
1169
|
+
switch (inv.status) {
|
|
1170
|
+
case "enforced":
|
|
1171
|
+
return `enforced by ${guard}`;
|
|
1172
|
+
case "partial":
|
|
1173
|
+
return `PARTIALLY ENFORCED: one half is covered, by ${guard}; the statement says which half is not`;
|
|
1174
|
+
case "open":
|
|
1175
|
+
return "NOT ENFORCED: nothing in the kit checks this" + (by.kind === "none" && by.until ? `, until ${by.until}` : "") + ". A rule you apply, not a guarantee you will be warned about";
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
function scopeOf(inv) {
|
|
1179
|
+
const { tags: tags2, targets, parts: parts2 } = inv.appliesTo;
|
|
1180
|
+
const bits = [];
|
|
1181
|
+
if (tags2) bits.push(`only ${tags2.join(", ")}`);
|
|
1182
|
+
if (targets) bits.push(`${targets.join(" / ")} delivery only`);
|
|
1183
|
+
if (parts2) bits.push(`parts ${parts2.join(", ")}`);
|
|
1184
|
+
return bits.join("; ");
|
|
1185
|
+
}
|
|
1186
|
+
function exampleLines(examples) {
|
|
1187
|
+
if (examples.length === 0) return [];
|
|
1188
|
+
const out = ["", "```js"];
|
|
1189
|
+
examples.forEach((ex, i) => {
|
|
1190
|
+
if (i > 0) out.push("");
|
|
1191
|
+
out.push("// WRONG", ex.wrong, "// RIGHT", ex.right);
|
|
1192
|
+
if (ex.note) out.push(...ex.note.split("\n").map((l) => `// ${l}`));
|
|
1193
|
+
});
|
|
1194
|
+
out.push("```");
|
|
1195
|
+
return out;
|
|
1196
|
+
}
|
|
1197
|
+
function coverageSummary(applicable) {
|
|
1198
|
+
const open = applicable.filter((i) => i.status === "open").length;
|
|
1199
|
+
const partial = applicable.filter((i) => i.status === "partial").length;
|
|
1200
|
+
if (open === 0 && partial === 0) return "";
|
|
1201
|
+
const total = applicable.length;
|
|
1202
|
+
const half = (n) => `only half of what ${n === 1 ? "it says" : "they say"}`;
|
|
1203
|
+
const clauses = [];
|
|
1204
|
+
if (open > 0) {
|
|
1205
|
+
clauses.push(`${open} of the ${total} below ${open === 1 ? "is" : "are"} enforced by NOTHING at all`);
|
|
1206
|
+
}
|
|
1207
|
+
if (partial > 0) {
|
|
1208
|
+
clauses.push(
|
|
1209
|
+
open > 0 ? `${partial} more by ${half(partial)}` : `${partial} of the ${total} below ${partial === 1 ? "is" : "are"} enforced by ${half(partial)}`
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
return ` ${clauses.join(", and ")}.`;
|
|
1213
|
+
}
|
|
1214
|
+
function catalogSectionLines(tag) {
|
|
1215
|
+
const applicable = invariantsFor(tag);
|
|
1216
|
+
const lines = [];
|
|
1217
|
+
if (applicable.length > 0) {
|
|
1218
|
+
lines.push(
|
|
1219
|
+
"",
|
|
1220
|
+
"### Invariants",
|
|
1221
|
+
"Rules that have already broken real consumers of this kit. Each block says what enforces it — read that line rather than assuming CI catches a violation, because nothing here reads YOUR code." + coverageSummary(applicable) + ' Diagnosis and wrong/right examples for every invariant are served once, not repeated per element — call component_reference with { name: "invariants" }.'
|
|
1222
|
+
);
|
|
1223
|
+
for (const inv of applicable) {
|
|
1224
|
+
const scope = scopeOf(inv);
|
|
1225
|
+
lines.push("", `#### ${inv.id}${scope ? ` (${scope})` : ""} — ${coverageOf(inv)}`, inv.statement);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
const recipes = recipesFor(tag);
|
|
1229
|
+
if (recipes.length > 0) {
|
|
1230
|
+
lines.push(
|
|
1231
|
+
"",
|
|
1232
|
+
"### Appears in surface recipes",
|
|
1233
|
+
'Compositions this element is part of, each with the whole ingredient list and the host wiring — nothing coordinates one element with another except your own code (host-coordinates). The WHY behind each wiring edge, and the caveats on the nesting below, are served once rather than repeated per ingredient — call component_reference with { name: "recipes" }.'
|
|
1234
|
+
);
|
|
1235
|
+
for (const r of recipes) {
|
|
1236
|
+
lines.push(
|
|
1237
|
+
"",
|
|
1238
|
+
`#### ${r.id} — ${r.intent}`,
|
|
1239
|
+
`- **Ingredients:** ${r.ingredients.map((t) => `\`<${t}>\``).join(", ")}`,
|
|
1240
|
+
`- **Delivery:** ${r.targets.join(", ")} · archetype: ${r.archetypes.join(", ")}`,
|
|
1241
|
+
`- **Backend:** your own endpoint, read with \`${r.backend.reader}\` from \`@kitn.ai/ui/wire\``,
|
|
1242
|
+
...compositionLines(r, ""),
|
|
1243
|
+
// Bare ids, deliberately. This row is a DEPENDENCY list, not a coverage
|
|
1244
|
+
// claim, and it can only ever be read a few lines below the full
|
|
1245
|
+
// `### Invariants` section in the same response, where each of these ids
|
|
1246
|
+
// already carries its own coverage line. Repeating the status here would
|
|
1247
|
+
// print it a second (and, with two recipes, a third) time per element for
|
|
1248
|
+
// no fact a reader does not already have on the page.
|
|
1249
|
+
`- **Invariants it leans on:** ${r.invariants.join(", ")}`,
|
|
1250
|
+
// Compact: the edge (event out of A, property into B) with no `note`.
|
|
1251
|
+
// The note is the WHY — often a full sentence, sometimes several — and
|
|
1252
|
+
// repeating it is the same shape of duplication task 4 removed from
|
|
1253
|
+
// invariants: this recipe's ingredients (kai-chat, kai-conversations,
|
|
1254
|
+
// kai-resizable, kai-artifact) all print this identical wiring list.
|
|
1255
|
+
// `renderRecipeAppendix` carries the notes exactly once.
|
|
1256
|
+
"- **Wiring** — event out of A, property into B, wired by the host:"
|
|
1257
|
+
);
|
|
1258
|
+
for (const w2 of r.wiring) {
|
|
1259
|
+
lines.push(` - \`<${w2.from}>\` fires \`${w2.event}\` → host sets \`${w2.to}.${w2.property}\``);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
return lines;
|
|
1264
|
+
}
|
|
1265
|
+
function renderRecipeAppendix() {
|
|
1266
|
+
const lines = [
|
|
1267
|
+
`## Surface recipe catalog (${surfaceRecipes.length} total) — wiring notes`,
|
|
1268
|
+
"",
|
|
1269
|
+
"Every element lookup that appears in a recipe lists its ingredients, delivery, backend and a compact wiring list (event out of A, property into B). This is the long form: the WHY behind each wiring edge, written once here rather than repeated on every ingredient."
|
|
1270
|
+
];
|
|
1271
|
+
for (const r of surfaceRecipes) {
|
|
1272
|
+
lines.push(
|
|
1273
|
+
"",
|
|
1274
|
+
`#### ${r.id} — ${r.intent}`,
|
|
1275
|
+
`- **Ingredients:** ${r.ingredients.map((t) => `\`<${t}>\``).join(", ")}`,
|
|
1276
|
+
`- **Delivery:** ${r.targets.join(", ")} · archetype: ${r.archetypes.join(", ")}`,
|
|
1277
|
+
`- **Backend:** your own endpoint, read with \`${r.backend.reader}\` from \`@kitn.ai/ui/wire\``,
|
|
1278
|
+
`- **Invariants it leans on:** ${r.invariants.join(", ")}`,
|
|
1279
|
+
...compositionLines(r, " — "),
|
|
1280
|
+
"- **Wiring** — event out of A, property into B, wired by the host:"
|
|
1281
|
+
);
|
|
1282
|
+
for (const w2 of r.wiring) {
|
|
1283
|
+
lines.push(
|
|
1284
|
+
` - \`<${w2.from}>\` fires \`${w2.event}\` → host sets \`${w2.to}.${w2.property}\`` + (w2.note ? ` — ${w2.note}` : "")
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return lines;
|
|
1289
|
+
}
|
|
1290
|
+
function renderInvariantAppendix() {
|
|
1291
|
+
const lines = [
|
|
1292
|
+
`## Invariant catalog (${invariants.length} total) — diagnosis and examples`,
|
|
1293
|
+
"",
|
|
1294
|
+
"Every element lookup lists the invariants that apply to it by id and one-line statement. This is the long form: full statement, diagnosis and wrong/right examples for each record, written once here rather than repeated on every element it applies to."
|
|
1295
|
+
];
|
|
1296
|
+
for (const inv of invariants) {
|
|
1297
|
+
const scope = scopeOf(inv);
|
|
1298
|
+
lines.push("", `#### ${inv.id}${scope ? ` (${scope})` : ""} — ${coverageOf(inv)}`, inv.statement);
|
|
1299
|
+
if (inv.diagnosis.length > 0) {
|
|
1300
|
+
lines.push("", "If you are debugging:");
|
|
1301
|
+
for (const d of inv.diagnosis) lines.push(`- ${d.symptom} → ${d.cause}`);
|
|
1302
|
+
}
|
|
1303
|
+
lines.push(...exampleLines(inv.examples));
|
|
1304
|
+
}
|
|
1305
|
+
return lines;
|
|
1306
|
+
}
|
|
1307
|
+
function eventDetail(typeText) {
|
|
1308
|
+
const m = /^CustomEvent<([\s\S]*)>$/.exec(typeText?.trim() ?? "");
|
|
1309
|
+
return m ? m[1].trim() || void 0 : void 0;
|
|
1310
|
+
}
|
|
645
1311
|
function formatReference(tag, provider) {
|
|
646
1312
|
const el = getElement(tag);
|
|
647
1313
|
if (!el) {
|
|
@@ -657,13 +1323,45 @@ Call component_reference with no name (or name: "list") to list all ${all.length
|
|
|
657
1323
|
if (el.description) {
|
|
658
1324
|
lines.push("", el.description.trim());
|
|
659
1325
|
}
|
|
1326
|
+
const entry = entryForTag(tag);
|
|
1327
|
+
const iface = el.name;
|
|
1328
|
+
const neverUpgrades = `If you skip registering it the element never upgrades: nothing renders, **no error and no warning is logged**, a property you set assigns and reads back correctly, and \`customElements.whenDefined('${tag}')\` never resolves.`;
|
|
1329
|
+
lines.push("", "### Getting the element");
|
|
1330
|
+
if (entry) {
|
|
1331
|
+
lines.push(
|
|
1332
|
+
`Register it before you use it. ${neverUpgrades}`,
|
|
1333
|
+
"",
|
|
1334
|
+
"```ts",
|
|
1335
|
+
"import '@kitn.ai/ui/elements';",
|
|
1336
|
+
`import '@kitn.ai/ui/elements/${entry}'; // or just this one`,
|
|
1337
|
+
"```"
|
|
1338
|
+
);
|
|
1339
|
+
} else {
|
|
1340
|
+
const optIn = optInEntryForTag(tag);
|
|
1341
|
+
lines.push(
|
|
1342
|
+
`**\`${tag}\` is opt-in — it is not part of \`import '@kitn.ai/ui/elements'\` (register-all), and that import alone will NOT register it.** ` + (optIn ? `Import its own entry point instead. ${neverUpgrades}` : (
|
|
1343
|
+
// No built module claims this tag, so there is no specifier to name.
|
|
1344
|
+
// Stay honest rather than inventing one.
|
|
1345
|
+
`Find its specific \`@kitn.ai/ui/elements/<name>\` entry point in the package's published exports before using it. ${neverUpgrades}`
|
|
1346
|
+
))
|
|
1347
|
+
);
|
|
1348
|
+
if (optIn) {
|
|
1349
|
+
lines.push("", "```ts", `import '@kitn.ai/ui/elements/${optIn}';`, "```");
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
if (iface) {
|
|
1353
|
+
lines.push(
|
|
1354
|
+
"",
|
|
1355
|
+
`TypeScript: \`import type { ${iface} } from '@kitn.ai/ui/elements';\` — the element interface ships with the package; do not hand-roll a structural type.`
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
660
1358
|
lines.push(
|
|
661
1359
|
"",
|
|
662
1360
|
"### AI/UI contract",
|
|
663
1361
|
'`kai-*` elements accept **array and object data as JavaScript properties** (set in JavaScript via `el.property = value`, not as HTML attributes). Events are native CustomEvents — listen with `el.addEventListener("event-name", handler)` and read `event.detail` for the payload.'
|
|
664
1362
|
);
|
|
665
1363
|
const cardType = cardTypeForTag(tag);
|
|
666
|
-
if (cardType !== void 0 &&
|
|
1364
|
+
if (cardType !== void 0 && ii(cardType)) {
|
|
667
1365
|
lines.push(...renderCardContract(tag, cardType, provider));
|
|
668
1366
|
} else if (cardHostTags().includes(tag)) {
|
|
669
1367
|
lines.push(...renderCardHost(tag));
|
|
@@ -701,7 +1399,10 @@ Call component_reference with no name (or name: "list") to list all ${all.length
|
|
|
701
1399
|
lines.push("", "### Events (CustomEvent, listen via addEventListener)");
|
|
702
1400
|
for (const ev of events) {
|
|
703
1401
|
const desc = ev.description?.trim() ?? "";
|
|
704
|
-
|
|
1402
|
+
const detail = eventDetail(ev.type?.text);
|
|
1403
|
+
lines.push(
|
|
1404
|
+
detail ? `- **${ev.name}** — ${desc} \`detail\`: \`${detail}\`` : `- **${ev.name}** — ${desc}`
|
|
1405
|
+
);
|
|
705
1406
|
}
|
|
706
1407
|
}
|
|
707
1408
|
const methods = (el.members ?? []).filter(isPublicMethod);
|
|
@@ -752,13 +1453,16 @@ Call component_reference with no name (or name: "list") to list all ${all.length
|
|
|
752
1453
|
}
|
|
753
1454
|
}
|
|
754
1455
|
}
|
|
1456
|
+
lines.push(...catalogSectionLines(tag));
|
|
755
1457
|
return lines.join("\n");
|
|
756
1458
|
}
|
|
757
1459
|
const reference = {
|
|
758
1460
|
name: "component_reference",
|
|
759
1461
|
description: "Look up AI/UI (kai-*) web components: their tags, props, events, imperative methods, and usage examples. For a card-backed element it also returns the card's JSON Schema and a ready-to-send tool definition generated from it — pass `provider` to get that provider's envelope.",
|
|
760
1462
|
inputSchema: z$1.object({
|
|
761
|
-
name: z$1.string().optional()
|
|
1463
|
+
name: z$1.string().optional().describe(
|
|
1464
|
+
'The element tag, e.g. "kai-chat". Omit it (or pass "list") to get the index of every element with a one-line summary, then ask again for the one you want. Pass "invariants" for the full diagnosis and wrong/right examples behind every invariant id an element lookup shows. Pass "recipes" for the full wiring notes behind every surface recipe an element lookup names.'
|
|
1465
|
+
),
|
|
762
1466
|
provider: z$1.enum(PROVIDERS).optional().describe(
|
|
763
1467
|
`Which tool-definition envelope to project for a card-backed element. Defaults to "${DEFAULT_PROVIDER}", the provider-neutral { name, description, schema } form.`
|
|
764
1468
|
)
|
|
@@ -782,17 +1486,25 @@ No tool definition was returned, because guessing which envelope you meant would
|
|
|
782
1486
|
const provider = rawProvider ?? DEFAULT_PROVIDER;
|
|
783
1487
|
let text2;
|
|
784
1488
|
if (!name || name === "list") {
|
|
785
|
-
const
|
|
786
|
-
const cardRows =
|
|
787
|
-
text2 = `AI/UI elements (${
|
|
1489
|
+
const tags2 = listElements();
|
|
1490
|
+
const cardRows = hi.map((t) => cardTagForType(t)).filter((t) => t !== void 0);
|
|
1491
|
+
text2 = `AI/UI elements (${tags2.length} total):
|
|
788
1492
|
|
|
789
|
-
` +
|
|
1493
|
+
` + tags2.map((t) => ` ${t}`).join("\n") + `
|
|
790
1494
|
|
|
791
1495
|
Call component_reference with a specific name (e.g. { name: "kai-chat" }) for full API details.
|
|
792
1496
|
|
|
793
1497
|
Card-backed elements — ask for one of these to get its JSON Schema and a generated \`${S}*\` tool definition:
|
|
794
1498
|
${cardRows.join(", ")}
|
|
795
|
-
The elements that HOST them (and carry the \`cardTypes\` / \`cardSchemas\` props): ${cardHostTags().join(", ")}
|
|
1499
|
+
The elements that HOST them (and carry the \`cardTypes\` / \`cardSchemas\` props): ${cardHostTags().join(", ")}
|
|
1500
|
+
|
|
1501
|
+
Call component_reference with { name: "invariants" } for the full diagnosis and wrong/right examples behind every invariant id shown on an element lookup.
|
|
1502
|
+
|
|
1503
|
+
Call component_reference with { name: "recipes" } for the full wiring notes behind every surface recipe an element lookup names.`;
|
|
1504
|
+
} else if (name === "invariants") {
|
|
1505
|
+
text2 = renderInvariantAppendix().join("\n");
|
|
1506
|
+
} else if (name === "recipes") {
|
|
1507
|
+
text2 = renderRecipeAppendix().join("\n");
|
|
796
1508
|
} else {
|
|
797
1509
|
text2 = formatReference(name, provider);
|
|
798
1510
|
}
|
|
@@ -1001,7 +1713,13 @@ const openai = {
|
|
|
1001
1713
|
// Both model and tools come from the browser. \`tools\` is undefined unless the
|
|
1002
1714
|
// front end declared any, and JSON.stringify drops it, so the same handler
|
|
1003
1715
|
// serves a tool archetype and a plain chat.
|
|
1004
|
-
|
|
1716
|
+
let chatBody: ChatRequestBody;
|
|
1717
|
+
try {
|
|
1718
|
+
chatBody = await readChatRequest(request);
|
|
1719
|
+
} catch (error) {
|
|
1720
|
+
return toChatErrorResponse(error);
|
|
1721
|
+
}
|
|
1722
|
+
const { model, messages, tools } = chatBody;
|
|
1005
1723
|
|
|
1006
1724
|
const upstream = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
1007
1725
|
method: 'POST',
|
|
@@ -1365,7 +2083,13 @@ function reframeToOpenAISse(body: ReadableStream<Uint8Array>): ReadableStream<Ui
|
|
|
1365
2083
|
async function chatHandler(request: Request): Promise<Response> {
|
|
1366
2084
|
// Both model and tools come from the browser. \`tools\` arrives in OpenAI
|
|
1367
2085
|
// function form and is converted to this API's shape below.
|
|
1368
|
-
|
|
2086
|
+
let chatBody: ChatRequestBody;
|
|
2087
|
+
try {
|
|
2088
|
+
chatBody = await readChatRequest(request);
|
|
2089
|
+
} catch (error) {
|
|
2090
|
+
return toChatErrorResponse(error);
|
|
2091
|
+
}
|
|
2092
|
+
const { model, messages, tools } = chatBody;
|
|
1369
2093
|
const { system, messages: anthropicMessages } = toAnthropicBody(messages);
|
|
1370
2094
|
const anthropicTools = toAnthropicTools(tools);
|
|
1371
2095
|
|
|
@@ -1492,7 +2216,13 @@ const openrouter = {
|
|
|
1492
2216
|
webRoute: `async function chatHandler(request: Request): Promise<Response> {
|
|
1493
2217
|
// tools is undefined unless the front end sent one; JSON.stringify drops it,
|
|
1494
2218
|
// so the same handler serves a tool archetype and a plain chat.
|
|
1495
|
-
|
|
2219
|
+
let chatBody: ChatRequestBody;
|
|
2220
|
+
try {
|
|
2221
|
+
chatBody = await readChatRequest(request);
|
|
2222
|
+
} catch (error) {
|
|
2223
|
+
return toChatErrorResponse(error);
|
|
2224
|
+
}
|
|
2225
|
+
const { model, messages, tools } = chatBody;
|
|
1496
2226
|
|
|
1497
2227
|
const upstream = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
1498
2228
|
method: 'POST',
|
|
@@ -1718,7 +2448,13 @@ const FINISH_REASONS: Record<string, string> = {
|
|
|
1718
2448
|
};
|
|
1719
2449
|
|
|
1720
2450
|
async function chatHandler(request: Request): Promise<Response> {
|
|
1721
|
-
|
|
2451
|
+
let chatBody: ChatRequestBody;
|
|
2452
|
+
try {
|
|
2453
|
+
chatBody = await readChatRequest(request);
|
|
2454
|
+
} catch (error) {
|
|
2455
|
+
return toChatErrorResponse(error);
|
|
2456
|
+
}
|
|
2457
|
+
const { messages, tools } = chatBody;
|
|
1722
2458
|
const toolSet = toToolSet(tools);
|
|
1723
2459
|
const prompt = toModelMessages(messages);
|
|
1724
2460
|
|
|
@@ -1966,7 +2702,13 @@ const agent = createReactAgent({
|
|
|
1966
2702
|
|
|
1967
2703
|
// Stream a compiled LangGraph agent to the browser as OpenAI-format SSE.
|
|
1968
2704
|
async function chatHandler(request: Request): Promise<Response> {
|
|
1969
|
-
|
|
2705
|
+
let chatBody: ChatRequestBody;
|
|
2706
|
+
try {
|
|
2707
|
+
chatBody = await readChatRequest(request);
|
|
2708
|
+
} catch (error) {
|
|
2709
|
+
return toChatErrorResponse(error);
|
|
2710
|
+
}
|
|
2711
|
+
const { messages } = chatBody;
|
|
1970
2712
|
|
|
1971
2713
|
// agent.stream() coerces plain {role, content} objects into BaseMessage
|
|
1972
2714
|
// instances itself, including OpenAI-shaped tool_calls, so the wire messages
|
|
@@ -2082,9 +2824,44 @@ import type { OpenAIWireMessage } from '@kitn.ai/ui/wire';
|
|
|
2082
2824
|
*/
|
|
2083
2825
|
type ChatRequestBody = { messages: OpenAIWireMessage[] };
|
|
2084
2826
|
|
|
2827
|
+
class ChatRequestError extends Error {
|
|
2828
|
+
constructor(readonly status: number, message: string) { super(message); }
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
/** Narrow the JSON body once, at the edge. A bare GET, a malformed body, or a
|
|
2832
|
+
* missing messages array is a ChatRequestError with a status — NEVER an
|
|
2833
|
+
* unhandled SyntaxError (findings F-10). */
|
|
2834
|
+
async function readChatRequest(request: Request): Promise<ChatRequestBody> {
|
|
2835
|
+
if (request.method !== 'POST') {
|
|
2836
|
+
throw new ChatRequestError(405, \`Method \${request.method} not allowed — POST /api/chat.\`);
|
|
2837
|
+
}
|
|
2838
|
+
let parsed: unknown;
|
|
2839
|
+
try { parsed = await request.json(); } catch {
|
|
2840
|
+
throw new ChatRequestError(400, 'Request body is not valid JSON.');
|
|
2841
|
+
}
|
|
2842
|
+
const body = parsed as ChatRequestBody;
|
|
2843
|
+
if (!Array.isArray(body?.messages)) {
|
|
2844
|
+
throw new ChatRequestError(400, 'Request body must carry a messages array.');
|
|
2845
|
+
}
|
|
2846
|
+
return body;
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
/** Map a guard rejection to the Response its status demands; rethrow anything
|
|
2850
|
+
* else — an unexpected error should be loud, not laundered into a 400. */
|
|
2851
|
+
function toChatErrorResponse(error: unknown): Response {
|
|
2852
|
+
if (error instanceof ChatRequestError) return Response.json({ error: error.message }, { status: error.status });
|
|
2853
|
+
throw error;
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2085
2856
|
export default {
|
|
2086
2857
|
async fetch(req: Request, env: Env): Promise<Response> {
|
|
2087
|
-
|
|
2858
|
+
let chatBody: ChatRequestBody;
|
|
2859
|
+
try {
|
|
2860
|
+
chatBody = await readChatRequest(req);
|
|
2861
|
+
} catch (error) {
|
|
2862
|
+
return toChatErrorResponse(error);
|
|
2863
|
+
}
|
|
2864
|
+
const { messages } = chatBody;
|
|
2088
2865
|
|
|
2089
2866
|
let nativeStream: ReadableStream<Uint8Array>;
|
|
2090
2867
|
try {
|
|
@@ -2159,7 +2936,13 @@ export default {
|
|
|
2159
2936
|
},
|
|
2160
2937
|
webRoute: `async function chatHandler(request: Request): Promise<Response> {
|
|
2161
2938
|
// Proxy Workers AI over its OpenAI-compatible HTTP endpoint, token server-side.
|
|
2162
|
-
|
|
2939
|
+
let chatBody: ChatRequestBody;
|
|
2940
|
+
try {
|
|
2941
|
+
chatBody = await readChatRequest(request);
|
|
2942
|
+
} catch (error) {
|
|
2943
|
+
return toChatErrorResponse(error);
|
|
2944
|
+
}
|
|
2945
|
+
const { messages } = chatBody;
|
|
2163
2946
|
|
|
2164
2947
|
const upstream = await fetch(
|
|
2165
2948
|
\`https://api.cloudflare.com/client/v4/accounts/\${process.env.CF_ACCOUNT_ID}/ai/v1/chat/completions\`,
|
|
@@ -2250,7 +3033,13 @@ const ollama = {
|
|
|
2250
3033
|
// The model is pinned here, not sent by the browser. tools IS forwarded:
|
|
2251
3034
|
// it is undefined unless the front end declared any, and JSON.stringify
|
|
2252
3035
|
// drops it, so the same handler serves a tool archetype and a plain chat.
|
|
2253
|
-
|
|
3036
|
+
let chatBody: ChatRequestBody;
|
|
3037
|
+
try {
|
|
3038
|
+
chatBody = await readChatRequest(request);
|
|
3039
|
+
} catch (error) {
|
|
3040
|
+
return toChatErrorResponse(error);
|
|
3041
|
+
}
|
|
3042
|
+
const { messages, tools } = chatBody;
|
|
2254
3043
|
|
|
2255
3044
|
const upstream = await fetch('http://localhost:11434/v1/chat/completions', {
|
|
2256
3045
|
method: 'POST',
|
|
@@ -2344,7 +3133,13 @@ const mastra = new MastraClient({ baseUrl: MASTRA_URL });
|
|
|
2344
3133
|
|
|
2345
3134
|
// Proxy a Mastra agent to the browser as OpenAI-format SSE.
|
|
2346
3135
|
async function chatHandler(request: Request): Promise<Response> {
|
|
2347
|
-
|
|
3136
|
+
let chatBody: ChatRequestBody;
|
|
3137
|
+
try {
|
|
3138
|
+
chatBody = await readChatRequest(request);
|
|
3139
|
+
} catch (error) {
|
|
3140
|
+
return toChatErrorResponse(error);
|
|
3141
|
+
}
|
|
3142
|
+
const { messages } = chatBody;
|
|
2348
3143
|
|
|
2349
3144
|
// MastraClient's Agent.stream() takes AI-SDK CoreMessage[], not the OpenAI
|
|
2350
3145
|
// wire format: each message has ONE literal role (not a union) and content
|
|
@@ -2508,9 +3303,64 @@ import type { OpenAIWireMessage } from '@kitn.ai/ui/wire';
|
|
|
2508
3303
|
const app = express();
|
|
2509
3304
|
app.use(express.json());
|
|
2510
3305
|
|
|
3306
|
+
class ChatRequestError extends Error {
|
|
3307
|
+
constructor(readonly status: number, message: string) { super(message); }
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
/** Narrow the JSON body once, at the edge. A bare GET or a missing messages
|
|
3311
|
+
* array is a ChatRequestError with a status — NEVER an unhandled throw
|
|
3312
|
+
* (findings F-10). Malformed JSON is rejected upstream by express.json(),
|
|
3313
|
+
* which the error handler below turns into a JSON 400. */
|
|
3314
|
+
function readChatRequest(req: express.Request): { messages: OpenAIWireMessage[] } {
|
|
3315
|
+
if (req.method !== 'POST') {
|
|
3316
|
+
throw new ChatRequestError(405, \`Method \${req.method} not allowed — POST /api/chat.\`);
|
|
3317
|
+
}
|
|
3318
|
+
const body = (req.body ?? {}) as { messages?: OpenAIWireMessage[] };
|
|
3319
|
+
if (!Array.isArray(body.messages)) {
|
|
3320
|
+
throw new ChatRequestError(400, 'Request body must carry a messages array.');
|
|
3321
|
+
}
|
|
3322
|
+
return body as { messages: OpenAIWireMessage[] };
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
/** Map a guard rejection to the response its status demands; rethrow anything
|
|
3326
|
+
* else — an unexpected error should be loud, not laundered into a 400. */
|
|
3327
|
+
function toChatErrorResponse(error: unknown, res: express.Response): void {
|
|
3328
|
+
if (error instanceof ChatRequestError) {
|
|
3329
|
+
res.status(error.status).json({ error: { message: error.message } });
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3332
|
+
throw error;
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
// express.json() rejects a malformed body at the MIDDLEWARE level, before any
|
|
3336
|
+
// handler runs — without this it surfaces as Express's default HTML error page.
|
|
3337
|
+
app.use(
|
|
3338
|
+
(
|
|
3339
|
+
err: { status?: number; message?: string },
|
|
3340
|
+
_req: express.Request,
|
|
3341
|
+
res: express.Response,
|
|
3342
|
+
_next: express.NextFunction,
|
|
3343
|
+
) => {
|
|
3344
|
+
res.status(err?.status ?? 500).json({ error: { message: err?.message ?? 'Request failed.' } });
|
|
3345
|
+
},
|
|
3346
|
+
);
|
|
3347
|
+
|
|
3348
|
+
// A bare GET is a 405 with the method named, not Express's default 404 —
|
|
3349
|
+
// the resource exists, the verb is wrong.
|
|
3350
|
+
app.get('/api/chat', (_req, res) => {
|
|
3351
|
+
res.status(405).json({ error: { message: 'Method not allowed — POST /api/chat.' } });
|
|
3352
|
+
});
|
|
3353
|
+
|
|
2511
3354
|
// POST /api/chat: bridge a Pi RPC session to the browser as SSE.
|
|
2512
3355
|
app.post('/api/chat', (req, res) => {
|
|
2513
|
-
|
|
3356
|
+
let chatBody: { messages: OpenAIWireMessage[] };
|
|
3357
|
+
try {
|
|
3358
|
+
chatBody = readChatRequest(req);
|
|
3359
|
+
} catch (error) {
|
|
3360
|
+
toChatErrorResponse(error, res);
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
const { messages } = chatBody;
|
|
2514
3364
|
const last = messages.at(-1)?.content;
|
|
2515
3365
|
// \`content\` is a plain string until the turn carries an attachment, at which
|
|
2516
3366
|
// point it is an ARRAY of content parts. Pi's RPC mode takes a TEXT prompt and
|
|
@@ -2715,24 +3565,86 @@ const mock = {
|
|
|
2715
3565
|
streamFormat: "native",
|
|
2716
3566
|
envVars: [],
|
|
2717
3567
|
routeTemplates: {},
|
|
2718
|
-
|
|
2719
|
-
|
|
3568
|
+
webRoute: `import { createMockResponder } from '@kitn.ai/ui/state';
|
|
3569
|
+
|
|
3570
|
+
// The kit's own mock responder — no provider, no key, no upstream. MODULE
|
|
3571
|
+
// scope, not per-request: the responder owns the cursor into its canned
|
|
3572
|
+
// replies, so rebuilding it per turn would answer with the first one forever.
|
|
3573
|
+
const mockResponse = createMockResponder();
|
|
3574
|
+
|
|
3575
|
+
async function chatHandler(request: Request): Promise<Response> {
|
|
3576
|
+
// The responder cycles its replies whatever you send, so the prompt is a
|
|
3577
|
+
// courtesy — but the body is still read exactly the way a real route reads
|
|
3578
|
+
// it, so swapping this handler for a provider's changes nothing upstream of
|
|
3579
|
+
// this file.
|
|
3580
|
+
let chatBody: ChatRequestBody;
|
|
3581
|
+
try {
|
|
3582
|
+
chatBody = await readChatRequest(request);
|
|
3583
|
+
} catch (error) {
|
|
3584
|
+
return toChatErrorResponse(error);
|
|
3585
|
+
}
|
|
3586
|
+
const { messages } = chatBody;
|
|
3587
|
+
const last = [...messages].reverse().find((m) => m.role === 'user');
|
|
3588
|
+
const prompt = last && typeof last.content === 'string' ? last.content : '';
|
|
3589
|
+
|
|
3590
|
+
// createMockResponder() yields COMPLETE OpenAI chat-completions SSE frames —
|
|
3591
|
+
// the same strings the front end's local preview streams — so this route only
|
|
3592
|
+
// writes them out verbatim. No framing is built here; the browser parses them
|
|
3593
|
+
// with readOpenAIStream from @kitn.ai/ui/wire, exactly as it would a real
|
|
3594
|
+
// route's response. Every frame is marked as a mock (a ': kai-mock' banner, a
|
|
3595
|
+
// _kai_mock field, model 'kai-mock', zero usage).
|
|
3596
|
+
const encoder = new TextEncoder();
|
|
3597
|
+
let open = true;
|
|
3598
|
+
const body = new ReadableStream<Uint8Array>({
|
|
3599
|
+
async start(controller) {
|
|
3600
|
+
for await (const frame of mockResponse(prompt)) {
|
|
3601
|
+
// The browser hangs up when the user asks a new question mid-answer;
|
|
3602
|
+
// stop producing frames rather than writing into a cancelled stream.
|
|
3603
|
+
if (!open) return;
|
|
3604
|
+
controller.enqueue(encoder.encode(frame));
|
|
3605
|
+
}
|
|
3606
|
+
controller.close();
|
|
3607
|
+
},
|
|
3608
|
+
cancel() {
|
|
3609
|
+
open = false;
|
|
3610
|
+
},
|
|
3611
|
+
});
|
|
3612
|
+
|
|
3613
|
+
return new Response(body, {
|
|
3614
|
+
status: 200,
|
|
3615
|
+
headers: {
|
|
3616
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
3617
|
+
// no-transform stops a proxy buffering the stream into one blob.
|
|
3618
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
3619
|
+
Connection: 'keep-alive',
|
|
3620
|
+
// A proxy in front of a preview build would otherwise buffer the whole
|
|
3621
|
+
// stream and the reply would land all at once instead of streaming.
|
|
3622
|
+
'X-Accel-Buffering': 'no',
|
|
3623
|
+
},
|
|
3624
|
+
});
|
|
3625
|
+
}`,
|
|
3626
|
+
streamMapping: "No backend and no provider, but a real wire. createMockResponder() from @kitn.ai/ui/state yields canned SSE frames and the scaffold reads them with readOpenAIStream from @kitn.ai/ui/wire, on the same code path a real route's response takes: same reader, same part folding, same abort handling. The frames carry the OpenAI chat-completions shape because the mock stands in for your /api/chat ROUTE, not for a provider (every other integration here re-frames its provider to that shape server-side), so going live replaces ONE expression: mockResponse(value) becomes the awaited response from your own chat route, and nothing else in the handler changes. Nothing here can be mistaken for a real turn: the stream opens with a ': kai-mock' SSE comment, every frame carries a _kai_mock field naming createMockResponder, model reports as 'kai-mock' (no provider serves it), and usage is all zeros. Block (2) serves the same frames over HTTP as an OPTIONAL route, so the /api/chat seam can be stood up before any provider exists.",
|
|
3627
|
+
runNote: "No backend or API key needed: the reply is generated in the browser and parsed by the same reader a real route feeds. Run the front-end as-is; swap `integration` for a real provider (openai, anthropic, openrouter, ollama) when ready, and the emitted handler differs by one expression. Block (2) is the OPTIONAL server half: the same mock frames served over HTTP, for standing up the /api/chat seam before any provider exists.",
|
|
2720
3628
|
docsSlug: "integrations/mock",
|
|
2721
|
-
// Nothing:
|
|
2722
|
-
//
|
|
2723
|
-
//
|
|
3629
|
+
// Nothing: the front end makes no HTTP request at all, and the optional route
|
|
3630
|
+
// reads only `messages` — and only as a courtesy prompt the responder is free
|
|
3631
|
+
// to ignore. There is no model to pick and no tools array anything would run.
|
|
2724
3632
|
forwardsFromClient: [],
|
|
2725
|
-
// Nothing to install:
|
|
3633
|
+
// Nothing to install: the route's one import is the kit itself, which every
|
|
3634
|
+
// scaffold already depends on (registry.test.ts pins deps.npm to the route's
|
|
3635
|
+
// imports minus @kitn.ai/ui).
|
|
2726
3636
|
deps: { npm: [], pip: [] },
|
|
2727
|
-
// The other 'frontend-safe' entry
|
|
2728
|
-
//
|
|
2729
|
-
//
|
|
2730
|
-
//
|
|
2731
|
-
// precisely why it still has to SAY so rather than be left blank.
|
|
3637
|
+
// The other 'frontend-safe' entry. The route now exists (G-04) but changes
|
|
3638
|
+
// nothing here: it holds no credential, sends no auth header and reaches no
|
|
3639
|
+
// upstream, so there is still nothing a public bundle could give away. The
|
|
3640
|
+
// schema's own refinement re-checks that claim against the route source.
|
|
2732
3641
|
keyExposure: "frontend-safe",
|
|
2733
|
-
// Nothing
|
|
2734
|
-
//
|
|
2735
|
-
// group of create-kai's gateway prompt
|
|
3642
|
+
// Nothing to supply out of band: no upstream, no process, no runtime. The
|
|
3643
|
+
// route is optional — the emitted app streams locally without it — which is
|
|
3644
|
+
// what keeps mock in the "No backend" group of create-kai's gateway prompt
|
|
3645
|
+
// (listGatewayGroups derives that from frontend-safe + outOfBand 'none', not
|
|
3646
|
+
// from the absence of a route, precisely because this route is one nobody
|
|
3647
|
+
// NEEDS).
|
|
2736
3648
|
outOfBand: "none"
|
|
2737
3649
|
};
|
|
2738
3650
|
const archetypes$1 = [
|
|
@@ -2764,9 +3676,41 @@ const archetypes$1 = [
|
|
|
2764
3676
|
defaultPlacement: "side",
|
|
2765
3677
|
docsSlug: "examples/agentic-assistant"
|
|
2766
3678
|
},
|
|
3679
|
+
/**
|
|
3680
|
+
* THE FIRST OFFICIAL BLOCK (recast spec 2026-08-20 § 3b; F-16).
|
|
3681
|
+
*
|
|
3682
|
+
* This preset used to carry `kai-artifact` + `kai-resizable` — an UNWIRED
|
|
3683
|
+
* artifact split — while omitting the conversation rail, the one thing its
|
|
3684
|
+
* name promised (rung-3 finding F-16). It now names the workspace BLOCK: the
|
|
3685
|
+
* chat-agnostic `kai-workspace` layout shell, a WIRED `kai-conversations`
|
|
3686
|
+
* rail, `kai-chat` in the main region, and the `@kitn.ai/ui/state` thread
|
|
3687
|
+
* helpers (bindThreadMessages / createThreadSessions / createSaveScheduler /
|
|
3688
|
+
* parseStoredThread) with the persistence POLICY left in consumer-owned
|
|
3689
|
+
* lines. `examples/apps/workspace/` is the block's reference implementation.
|
|
3690
|
+
*
|
|
3691
|
+
* The artifact split did not vanish — it moved to `artifact-split` below, so
|
|
3692
|
+
* its renderer branch keeps its cells in `verify:scaffold`.
|
|
3693
|
+
*/
|
|
2767
3694
|
{
|
|
2768
3695
|
id: "workspace",
|
|
2769
3696
|
title: "Agentic workspace",
|
|
3697
|
+
components: ["kai-chat", "kai-workspace", "kai-conversations"],
|
|
3698
|
+
defaultPlacement: "full-page",
|
|
3699
|
+
docsSlug: "examples/workspace"
|
|
3700
|
+
},
|
|
3701
|
+
/**
|
|
3702
|
+
* THE PRESET THAT KEEPS A CAPABILITY ON THE AXIS — the same reason the
|
|
3703
|
+
* `attachments` preset exists (see its comment below): `listCapabilityGroups`
|
|
3704
|
+
* derives the gate's surface axis from this table, so when the recast moved
|
|
3705
|
+
* the artifact pair off `workspace`, this entry is what kept
|
|
3706
|
+
* `kai-artifact`+`kai-resizable` compiling in `verify:scaffold` instead of
|
|
3707
|
+
* silently losing every cell. The pair is ONE capability because `isArtifactSplit`
|
|
3708
|
+
* in scaffold.ts requires both: a split with no preview pane is an empty
|
|
3709
|
+
* panel, and a preview with no split has nowhere to sit.
|
|
3710
|
+
*/
|
|
3711
|
+
{
|
|
3712
|
+
id: "artifact-split",
|
|
3713
|
+
title: "Artifact preview split",
|
|
2770
3714
|
components: ["kai-chat", "kai-artifact", "kai-resizable"],
|
|
2771
3715
|
defaultPlacement: "side",
|
|
2772
3716
|
docsSlug: "examples/workspace"
|
|
@@ -2868,9 +3812,33 @@ const CHAT_REQUEST_BODY_DECL = [
|
|
|
2868
3812
|
` tools?: unknown[];`,
|
|
2869
3813
|
`};`,
|
|
2870
3814
|
``,
|
|
2871
|
-
|
|
3815
|
+
`class ChatRequestError extends Error {`,
|
|
3816
|
+
` constructor(readonly status: number, message: string) { super(message); }`,
|
|
3817
|
+
`}`,
|
|
3818
|
+
``,
|
|
3819
|
+
`/** Narrow the JSON body once, at the edge. A bare GET, a malformed body, or a`,
|
|
3820
|
+
` * missing messages array is a ChatRequestError with a status — NEVER an`,
|
|
3821
|
+
` * unhandled SyntaxError: one killed a Vite dev server (findings F-10). */`,
|
|
2872
3822
|
`async function readChatRequest(request: Request): Promise<ChatRequestBody> {`,
|
|
2873
|
-
`
|
|
3823
|
+
` if (request.method !== 'POST') {`,
|
|
3824
|
+
` throw new ChatRequestError(405, \`Method \${request.method} not allowed — POST /api/chat.\`);`,
|
|
3825
|
+
` }`,
|
|
3826
|
+
` let parsed: unknown;`,
|
|
3827
|
+
` try { parsed = await request.json(); } catch {`,
|
|
3828
|
+
` throw new ChatRequestError(400, 'Request body is not valid JSON.');`,
|
|
3829
|
+
` }`,
|
|
3830
|
+
` const body = parsed as ChatRequestBody;`,
|
|
3831
|
+
` if (!Array.isArray(body?.messages)) {`,
|
|
3832
|
+
` throw new ChatRequestError(400, 'Request body must carry a messages array.');`,
|
|
3833
|
+
` }`,
|
|
3834
|
+
` return body;`,
|
|
3835
|
+
`}`,
|
|
3836
|
+
``,
|
|
3837
|
+
`/** Map a guard rejection to the Response its status demands; rethrow anything`,
|
|
3838
|
+
` * else — an unexpected error should be loud, not laundered into a 400. */`,
|
|
3839
|
+
`function toChatErrorResponse(error: unknown): Response {`,
|
|
3840
|
+
` if (error instanceof ChatRequestError) return Response.json({ error: error.message }, { status: error.status });`,
|
|
3841
|
+
` throw error;`,
|
|
2874
3842
|
`}`
|
|
2875
3843
|
];
|
|
2876
3844
|
const CONTENT_PARTS_DECL = [
|
|
@@ -3022,8 +3990,9 @@ const text = (s) => ({
|
|
|
3022
3990
|
content: [{ type: "text", text: s }]
|
|
3023
3991
|
});
|
|
3024
3992
|
const FLEX_FILL = "flex: 1; min-height: 0;";
|
|
3993
|
+
const TOAST_LAYER_NOTE = "The z-index stays BELOW 100: the kit's kai-toast-region (a body-level sibling toast() mounts) paints at z-index var(--kai-toast-z, 100), so a wrapper at or above it buries every toast.";
|
|
3025
3994
|
const FULL_PAGE = {
|
|
3026
|
-
style: "position: fixed; inset: 0; display: flex; flex-direction: column; text-align: start; z-index:
|
|
3995
|
+
style: "position: fixed; inset: 0; display: flex; flex-direction: column; text-align: start; z-index: 90;",
|
|
3027
3996
|
chatFill: FLEX_FILL,
|
|
3028
3997
|
note: "fills the viewport (fixed, inset 0)",
|
|
3029
3998
|
altNote: [
|
|
@@ -3033,6 +4002,7 @@ const FULL_PAGE = {
|
|
|
3033
4002
|
"TanStack Start starter wraps every route in a Header + Footer that pushed the composer 13px",
|
|
3034
4003
|
"below the fold at 1280x800. Fixed positioning escapes both, `text-align: start` undoes the",
|
|
3035
4004
|
"inherited centring, and z-index keeps a sticky header from painting over the thread.",
|
|
4005
|
+
TOAST_LAYER_NOTE,
|
|
3036
4006
|
"This DOES cover the chrome around it (nav included) — that is what full-page means here.",
|
|
3037
4007
|
'Want the chat to sit INSIDE your own layout instead? Use placement: "inline".'
|
|
3038
4008
|
]
|
|
@@ -3049,18 +4019,20 @@ function placementStyle(placement) {
|
|
|
3049
4019
|
};
|
|
3050
4020
|
case "side":
|
|
3051
4021
|
return {
|
|
3052
|
-
style: "position: fixed; top: 0; inset-inline-end: 0; height: 100dvh; width: 380px; border-inline-start: 1px solid var(--kai-color-border); display: flex; flex-direction: column; z-index:
|
|
4022
|
+
style: "position: fixed; top: 0; inset-inline-end: 0; height: 100dvh; width: 380px; border-inline-start: 1px solid var(--kai-color-border); display: flex; flex-direction: column; z-index: 90;",
|
|
3053
4023
|
chatFill: FLEX_FILL,
|
|
3054
4024
|
note: "full-height side panel, docked to the trailing edge (100dvh)",
|
|
3055
4025
|
altNote: [
|
|
4026
|
+
TOAST_LAYER_NOTE,
|
|
3056
4027
|
"In-flow alternative (push content instead of overlay): drop `position`/`z-index` and make this a `flex: 0 0 380px` column inside a `display: flex` row at `height: 100dvh`."
|
|
3057
4028
|
]
|
|
3058
4029
|
};
|
|
3059
4030
|
case "docked-widget":
|
|
3060
4031
|
return {
|
|
3061
|
-
style: "position: fixed; bottom: 1.5rem; inset-inline-end: 1.5rem; width: 380px; height: 600px; max-height: calc(100dvh - 3rem); border-radius: 16px; overflow: hidden; box-shadow: 0 12px 32px var(--kai-shadow-color, rgba(0,0,0,0.18)); display: flex; flex-direction: column; z-index:
|
|
4032
|
+
style: "position: fixed; bottom: 1.5rem; inset-inline-end: 1.5rem; width: 380px; height: 600px; max-height: calc(100dvh - 3rem); border-radius: 16px; overflow: hidden; box-shadow: 0 12px 32px var(--kai-shadow-color, rgba(0,0,0,0.18)); display: flex; flex-direction: column; z-index: 90;",
|
|
3062
4033
|
chatFill: FLEX_FILL,
|
|
3063
|
-
note: "fixed, floating bottom-right widget"
|
|
4034
|
+
note: "fixed, floating bottom-right widget",
|
|
4035
|
+
altNote: [TOAST_LAYER_NOTE]
|
|
3064
4036
|
};
|
|
3065
4037
|
default:
|
|
3066
4038
|
return FULL_PAGE;
|
|
@@ -3122,8 +4094,10 @@ function realStreamBody(opts) {
|
|
|
3122
4094
|
cards = false,
|
|
3123
4095
|
valueSource = "e.detail.value",
|
|
3124
4096
|
afterValue = [],
|
|
4097
|
+
afterTurn = [],
|
|
3125
4098
|
mock: mock2 = false,
|
|
3126
|
-
filesExpr
|
|
4099
|
+
filesExpr,
|
|
4100
|
+
fetchSignal
|
|
3127
4101
|
} = opts;
|
|
3128
4102
|
const asConst = strictRoles ? " as const" : "";
|
|
3129
4103
|
const historyType = strictRoles ? ": ChatMessage[]" : "";
|
|
@@ -3156,6 +4130,11 @@ function realStreamBody(opts) {
|
|
|
3156
4130
|
`${indent} method: 'POST',`,
|
|
3157
4131
|
`${indent} headers: { 'Content-Type': 'application/json' },`,
|
|
3158
4132
|
`${indent} body: JSON.stringify(${bodyPayload(threadExpr)}),`,
|
|
4133
|
+
...fetchSignal ? [
|
|
4134
|
+
`${indent} // The turn's own AbortSignal (from sessions.begin above): switching`,
|
|
4135
|
+
`${indent} // threads or deleting this one can cancel the request itself.`,
|
|
4136
|
+
`${indent} signal: ${fetchSignal},`
|
|
4137
|
+
] : [],
|
|
3159
4138
|
`${indent}});`,
|
|
3160
4139
|
`${indent}// The finished turn: text, reasoning, tool calls, stop reason, usage. An`,
|
|
3161
4140
|
`${indent}// error FRAME inside a 200 stream lands on turn.error, and whatever`,
|
|
@@ -3180,17 +4159,29 @@ function realStreamBody(opts) {
|
|
|
3180
4159
|
`${pad} if (turn.error) console.error('Model error:', turn.error.message);`
|
|
3181
4160
|
],
|
|
3182
4161
|
`${pad}} catch (err) {`,
|
|
4162
|
+
...fetchSignal && !mock2 ? [
|
|
4163
|
+
`${pad} // A DELIBERATE abort (the thread was deleted, or a new turn began on it)`,
|
|
4164
|
+
`${pad} // is not a failure and has no one left to report it to.`,
|
|
4165
|
+
`${pad} if (${fetchSignal}.aborted) return;`
|
|
4166
|
+
] : [],
|
|
3183
4167
|
`${pad} // Without this a bad key is a permanently blank assistant bubble plus an`,
|
|
3184
|
-
`${pad} // unhandled rejection. abort() settles the message
|
|
3185
|
-
`${pad} //
|
|
3186
|
-
`${pad} //
|
|
3187
|
-
`${pad}
|
|
3188
|
-
`${pad}
|
|
4168
|
+
`${pad} // unhandled rejection. abort(reason) settles the message AND puts the`,
|
|
4169
|
+
`${pad} // reason where the reader can see it: onto any tool panel still waiting`,
|
|
4170
|
+
`${pad} // on a result (output-error, so nothing spins forever), or — on a`,
|
|
4171
|
+
`${pad} // text-only turn, which has no panel to carry it — as a new text part in`,
|
|
4172
|
+
`${pad} // the thread. Text that already streamed stays put.`,
|
|
4173
|
+
`${pad} // The reason is SHOWN TO THE USER. err.message is the honest default;`,
|
|
4174
|
+
`${pad} // swap in a sentence a visitor can read once you know your failure modes.`,
|
|
4175
|
+
`${pad} // The && is load-bearing: an Error is free to carry an empty message, and`,
|
|
4176
|
+
`${pad} // an empty reason is nothing to show — back to the blank bubble.`,
|
|
4177
|
+
`${pad} stream.abort(err instanceof Error && err.message ? err.message : 'Request failed');`,
|
|
4178
|
+
`${pad} console.error(err); // the console is for you; the thread is for them`,
|
|
3189
4179
|
`${pad}} finally {`,
|
|
3190
4180
|
`${pad} // done() SETTLES the message: every sink call after it is dropped, which`,
|
|
3191
4181
|
`${pad} // is why the whole loop runs above it and not after.`,
|
|
3192
4182
|
`${pad} stream.done();`,
|
|
3193
4183
|
`${pad} ${setLoading("false")}`,
|
|
4184
|
+
...afterTurn.map((l) => `${pad} ${l}`),
|
|
3194
4185
|
`${pad}}`
|
|
3195
4186
|
].join("\n");
|
|
3196
4187
|
}
|
|
@@ -3276,12 +4267,14 @@ function assertCardToolFormat(integration) {
|
|
|
3276
4267
|
return provider;
|
|
3277
4268
|
}
|
|
3278
4269
|
function wireImportLines(opts) {
|
|
3279
|
-
const { pad = "", typed, toolLoop = false, setMessagesType = false, cards = false, cardTools: emitsCardTools = false, mock: mock2 = false, attachments = false } = opts;
|
|
4270
|
+
const { pad = "", typed, toolLoop = false, setMessagesType = false, cards = false, cardTools: emitsCardTools = false, mock: mock2 = false, attachments = false, workspaceBlock = false } = opts;
|
|
3280
4271
|
const stateNames = [
|
|
4272
|
+
...workspaceBlock ? ["bindThreadMessages"] : [],
|
|
3281
4273
|
"createAssistantStream",
|
|
3282
4274
|
// The mock's canned reply comes from the kit, not from a copy pasted into
|
|
3283
4275
|
// this file. One implementation, shared with create-kai and the starters.
|
|
3284
4276
|
...mock2 ? ["createMockResponder"] : [],
|
|
4277
|
+
...workspaceBlock ? ["createSaveScheduler", "createThreadSessions", "parseStoredThread"] : [],
|
|
3285
4278
|
...attachments ? ["type AttachmentData"] : [],
|
|
3286
4279
|
...typed ? ["type ChatMessage"] : [],
|
|
3287
4280
|
...typed && setMessagesType ? ["type SetMessages"] : []
|
|
@@ -3409,11 +4402,197 @@ const CARD_PROP_COMMENT = [
|
|
|
3409
4402
|
function jsxComment(lines, pad) {
|
|
3410
4403
|
return lines.map((l) => `${pad}{/* ${l.replace(/^\/\/ ?/, "")} */}`);
|
|
3411
4404
|
}
|
|
4405
|
+
function indentEmitted(lines, by) {
|
|
4406
|
+
return lines.flatMap((l) => l.split("\n")).map((l) => l === "" ? l : `${by}${l}`);
|
|
4407
|
+
}
|
|
3412
4408
|
const MESSAGE_EMBEDDED_TAGS = /* @__PURE__ */ new Set(["kai-tool", "kai-reasoning"]);
|
|
3413
|
-
const
|
|
3414
|
-
function
|
|
4409
|
+
const ARTIFACT_SPLIT_TAGS = /* @__PURE__ */ new Set(["kai-resizable", "kai-artifact"]);
|
|
4410
|
+
function isArtifactSplit(components) {
|
|
3415
4411
|
return components.includes("kai-resizable") && components.includes("kai-artifact");
|
|
3416
4412
|
}
|
|
4413
|
+
const BLOCK_TAGS = /* @__PURE__ */ new Set(["kai-workspace", "kai-conversations"]);
|
|
4414
|
+
function isWorkspaceBlock(components) {
|
|
4415
|
+
return components.includes("kai-workspace") && components.includes("kai-conversations");
|
|
4416
|
+
}
|
|
4417
|
+
function blockThreadStoreLines(pad = "") {
|
|
4418
|
+
return [
|
|
4419
|
+
`${pad}// ── The workspace block's thread store — YOUR code, from here down ─────────`,
|
|
4420
|
+
`${pad}//`,
|
|
4421
|
+
`${pad}// The record shape is yours (title policy, timestamps, whatever you add);`,
|
|
4422
|
+
`${pad}// the kit's helpers only require { id, messages } (ThreadLike).`,
|
|
4423
|
+
`${pad}interface Thread {`,
|
|
4424
|
+
`${pad} id: string;`,
|
|
4425
|
+
`${pad} title: string;`,
|
|
4426
|
+
`${pad} updatedAt: string;`,
|
|
4427
|
+
`${pad} messages: ChatMessage[];`,
|
|
4428
|
+
`${pad}}`,
|
|
4429
|
+
``,
|
|
4430
|
+
`${pad}const THREADS_KEY = 'kai-workspace/threads/v1';`,
|
|
4431
|
+
``,
|
|
4432
|
+
`${pad}// PERSISTENCE IS YOURS. localStorage here is a starting policy, not kit`,
|
|
4433
|
+
`${pad}// behavior: the backend, the quota, what is retained and for how long are`,
|
|
4434
|
+
`${pad}// application decisions. Swap these two functions for your API and nothing`,
|
|
4435
|
+
`${pad}// else in the block changes.`,
|
|
4436
|
+
`${pad}function loadThreads(): Thread[] {`,
|
|
4437
|
+
`${pad} let raw: string | null = null;`,
|
|
4438
|
+
`${pad} // Private-mode Safari throws on ACCESS: no storage is a working app with`,
|
|
4439
|
+
`${pad} // no history, not a crash on first paint.`,
|
|
4440
|
+
`${pad} try { raw = localStorage.getItem(THREADS_KEY); } catch { return []; }`,
|
|
4441
|
+
`${pad} if (!raw) return [];`,
|
|
4442
|
+
`${pad} try {`,
|
|
4443
|
+
`${pad} const stored: unknown = JSON.parse(raw);`,
|
|
4444
|
+
`${pad} if (!Array.isArray(stored)) return [];`,
|
|
4445
|
+
`${pad} return stored.flatMap((record): Thread[] => {`,
|
|
4446
|
+
`${pad} if (typeof record !== 'object' || record === null) return [];`,
|
|
4447
|
+
`${pad} const r = record as { id?: unknown; title?: unknown; updatedAt?: unknown; messages?: unknown };`,
|
|
4448
|
+
`${pad} if (typeof r.id !== 'string' || r.id === '') return [];`,
|
|
4449
|
+
`${pad} // The kit validates the MESSAGES (the shape it owns — the MessagePart`,
|
|
4450
|
+
`${pad} // variant list is derived, not hand-typed) and reports the drops;`,
|
|
4451
|
+
`${pad} // deciding what a drop means is this line, and it is yours.`,
|
|
4452
|
+
`${pad} const { messages, dropped } = parseStoredThread(r.messages);`,
|
|
4453
|
+
`${pad} if (dropped.length > 0) console.warn('[threads] dropped unreadable stored entries', dropped);`,
|
|
4454
|
+
`${pad} return [{`,
|
|
4455
|
+
`${pad} id: r.id,`,
|
|
4456
|
+
`${pad} title: typeof r.title === 'string' ? r.title : 'New conversation',`,
|
|
4457
|
+
`${pad} updatedAt: typeof r.updatedAt === 'string' ? r.updatedAt : new Date().toISOString(),`,
|
|
4458
|
+
`${pad} messages,`,
|
|
4459
|
+
`${pad} }];`,
|
|
4460
|
+
`${pad} });`,
|
|
4461
|
+
`${pad} } catch {`,
|
|
4462
|
+
`${pad} // Unparseable history is ignored, loudly enough to notice in dev.`,
|
|
4463
|
+
`${pad} console.warn('[threads] stored history could not be parsed and was ignored');`,
|
|
4464
|
+
`${pad} return [];`,
|
|
4465
|
+
`${pad} }`,
|
|
4466
|
+
`${pad}}`,
|
|
4467
|
+
``,
|
|
4468
|
+
`${pad}function saveThreads(threads: Thread[]): void {`,
|
|
4469
|
+
`${pad} // QuotaExceededError is the realistic failure. Warn-and-continue is a`,
|
|
4470
|
+
`${pad} // POLICY (yours): the alternative — throwing out of a save mid-stream —`,
|
|
4471
|
+
`${pad} // would take the thread down with it.`,
|
|
4472
|
+
`${pad} try { localStorage.setItem(THREADS_KEY, JSON.stringify(threads)); }`,
|
|
4473
|
+
`${pad} catch (err) { console.warn('[threads] history could not be saved', err); }`,
|
|
4474
|
+
`${pad}}`,
|
|
4475
|
+
``,
|
|
4476
|
+
`${pad}// First line of the first user turn, clipped — the fallback titling policy.`,
|
|
4477
|
+
`${pad}// Titles are what the rail's built-in search matches, so this is also the`,
|
|
4478
|
+
`${pad}// search index; swap in your own (a summariser, a user rename).`,
|
|
4479
|
+
`${pad}function deriveTitle(text: string): string {`,
|
|
4480
|
+
`${pad} const firstLine = text.trim().split('\\n', 1)[0] ?? '';`,
|
|
4481
|
+
`${pad} if (firstLine === '') return 'New conversation';`,
|
|
4482
|
+
`${pad} return firstLine.length > 48 ? firstLine.slice(0, 47) + '…' : firstLine;`,
|
|
4483
|
+
`${pad}}`
|
|
4484
|
+
];
|
|
4485
|
+
}
|
|
4486
|
+
const BLOCK_RAIL_NOTE = [
|
|
4487
|
+
`The rail renders the ROWS it is handed (batteries mode) — a view of your`,
|
|
4488
|
+
`records, no message bodies. To own the row markup instead, slot your own`,
|
|
4489
|
+
`<kai-conversation-item> loop as light-DOM children (item mode): data rows`,
|
|
4490
|
+
`stop rendering while any item child is present, and selection still arrives`,
|
|
4491
|
+
`as kai-conversation-select.`
|
|
4492
|
+
];
|
|
4493
|
+
const BLOCK_RAIL_NOTE_SOLID = [
|
|
4494
|
+
`The rail renders the ROWS it is handed (batteries mode) — a view of your`,
|
|
4495
|
+
`records, no message bodies. To own the row markup instead, pass your own`,
|
|
4496
|
+
`SlottedConversationItem loop via the \`items\` prop (item mode): data rows`,
|
|
4497
|
+
`stop rendering while items are present, and selection still arrives through`,
|
|
4498
|
+
`onSelect.`
|
|
4499
|
+
];
|
|
4500
|
+
const BLOCK_TARGET_ID_NOTE = [
|
|
4501
|
+
`The thread that was open when the user hit send — not whichever is open`,
|
|
4502
|
+
`when the tokens land. Everything below binds to this id, which is what`,
|
|
4503
|
+
`makes switching away mid-reply safe.`
|
|
4504
|
+
];
|
|
4505
|
+
const BLOCK_BIND_NOTE = [
|
|
4506
|
+
`bindThreadMessages routes every stream delta to THIS thread, with the`,
|
|
4507
|
+
`reactivity two-halves (new array + new object) handled by the helper.`,
|
|
4508
|
+
`\`touch\` is your policy hook — here it stamps updatedAt on every delta.`
|
|
4509
|
+
];
|
|
4510
|
+
const BLOCK_SESSIONS_NOTE = [
|
|
4511
|
+
`One in-flight turn per thread: begin() aborts a prior turn on the same`,
|
|
4512
|
+
`thread and hands back its AbortController. Deleting a thread mid-stream is`,
|
|
4513
|
+
`sessions.abort(id) — the aborted turn's late deltas are dropped by the`,
|
|
4514
|
+
`bound sink instead of resurrecting the record.`
|
|
4515
|
+
];
|
|
4516
|
+
function blockLiveThread(readExpr) {
|
|
4517
|
+
return {
|
|
4518
|
+
open: ({ pad, userMessage }) => [
|
|
4519
|
+
`${pad}// The STORE is the thread: the id-bound sink writes each delta back into`,
|
|
4520
|
+
`${pad}// the record, so every round below re-encodes the live, current value.`,
|
|
4521
|
+
`${pad}setMessages(() => [...${readExpr}, ${userMessage}]);`
|
|
4522
|
+
],
|
|
4523
|
+
live: readExpr,
|
|
4524
|
+
setter: "setMessages"
|
|
4525
|
+
};
|
|
4526
|
+
}
|
|
4527
|
+
function slotPlacementFor(tag) {
|
|
4528
|
+
for (const r of surfaceRecipes) {
|
|
4529
|
+
for (const c of r.composition ?? []) {
|
|
4530
|
+
if (c.child === tag) return { parent: c.parent, slot: c.slot };
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
return null;
|
|
4534
|
+
}
|
|
4535
|
+
const ALWAYS_EMITTED_TAG = "kai-chat";
|
|
4536
|
+
function slottedInChat(components) {
|
|
4537
|
+
return components.flatMap((tag) => {
|
|
4538
|
+
const placement = slotPlacementFor(tag);
|
|
4539
|
+
if (!placement || placement.parent !== ALWAYS_EMITTED_TAG) return [];
|
|
4540
|
+
return [{ tag, slot: placement.slot }];
|
|
4541
|
+
});
|
|
4542
|
+
}
|
|
4543
|
+
function railSiblingNote(tag, placement) {
|
|
4544
|
+
const w2 = conversationsWiring();
|
|
4545
|
+
const handler = `on${toPascalCase(w2.event)}`;
|
|
4546
|
+
return [
|
|
4547
|
+
` {/* NOT WIRED, and NOT SLOTTED — both deliberate, both fixable here.`,
|
|
4548
|
+
` COMPOSITION: the kit composes this as <${tag} slot="${placement.slot}"> INSIDE`,
|
|
4549
|
+
` <${placement.parent}>, which renders it into a fixed-width ::part(${placement.slot}) column.`,
|
|
4550
|
+
` The generated React wrappers take no \`slot\` prop (see WebComponentProps`,
|
|
4551
|
+
` in @kitn.ai/ui/react), so this renders as a sibling instead. Drop to the`,
|
|
4552
|
+
` raw <${placement.parent}> custom element if you want the slotted layout.`,
|
|
4553
|
+
` WIRING: set ${w2.property}={...} and ${handler}={...} — the handler`,
|
|
4554
|
+
` reads event.detail.id and calls setMessages with that thread. */}`
|
|
4555
|
+
];
|
|
4556
|
+
}
|
|
4557
|
+
function htmlComment(lines, pad) {
|
|
4558
|
+
return lines.map(
|
|
4559
|
+
(l, i) => `${pad}${i === 0 ? "<!-- " : " "}${l}${i === lines.length - 1 ? " -->" : ""}`
|
|
4560
|
+
);
|
|
4561
|
+
}
|
|
4562
|
+
function slottedChildMarkup(components, pad, opts) {
|
|
4563
|
+
const w2 = conversationsWiring();
|
|
4564
|
+
return slottedInChat(components).flatMap((s) => [
|
|
4565
|
+
// What the shell DOES, and — the half a comment gets wrong by being generous
|
|
4566
|
+
// — what it does not. `<kai-chat>`'s sidebar region is one fixed-width aside
|
|
4567
|
+
// (`chat-thread.tsx`), exposed as a part and nothing more: it does not listen
|
|
4568
|
+
// for the rail's collapse, and it has no responsive behaviour, so a rail that
|
|
4569
|
+
// collapses inside it leaves the column exactly as wide as it was.
|
|
4570
|
+
...htmlComment(
|
|
4571
|
+
[
|
|
4572
|
+
`SLOTTED, not a sibling: <kai-chat> renders this into its own`,
|
|
4573
|
+
`::part(${s.slot}) aside — a fixed-width column, and that is the whole of`,
|
|
4574
|
+
`what the shell does with it. Collapsing the rail does NOT narrow the`,
|
|
4575
|
+
`column: use the <kai-workspace> layout shell (start aside: startCollapsed /`,
|
|
4576
|
+
`collapseBelow / drawer-below) or your own layout if it has to collapse or resize.${opts.wired ? " src/main.ts drives the rail." : ""}`
|
|
4577
|
+
],
|
|
4578
|
+
pad
|
|
4579
|
+
),
|
|
4580
|
+
...opts.wired ? [] : htmlComment(notWiredNote(s.tag, w2.property, w2.event), pad),
|
|
4581
|
+
`${pad}<${s.tag}${opts.ids ? ` id="${s.tag.replace(/^kai-/, "")}"` : ""} slot="${s.slot}" style="display:block;height:100%"></${s.tag}>`
|
|
4582
|
+
]);
|
|
4583
|
+
}
|
|
4584
|
+
function notWiredNote(tag, property, event) {
|
|
4585
|
+
return [
|
|
4586
|
+
`NOT WIRED: <${tag}> needs el.${property} (a JS property — arrays are never`,
|
|
4587
|
+
`attributes) and a ${event} listener that sets kai-chat.messages from your own`,
|
|
4588
|
+
`thread store. The html target emits that wiring in full; ask the`,
|
|
4589
|
+
`component_reference MCP tool for the workspace-chat recipe to see the shape.`
|
|
4590
|
+
];
|
|
4591
|
+
}
|
|
4592
|
+
function conversationsWiring() {
|
|
4593
|
+
const edge = surfaceRecipes.flatMap((r) => r.wiring).find((w2) => w2.from === "kai-conversations" && w2.to === "kai-chat" && w2.property === "messages");
|
|
4594
|
+
return { property: "conversations", event: edge?.event ?? "kai-conversation-select" };
|
|
4595
|
+
}
|
|
3417
4596
|
const ATTACHMENT_TAGS = /* @__PURE__ */ new Set(["kai-file-upload", "kai-attachments"]);
|
|
3418
4597
|
function hasAttachments(components) {
|
|
3419
4598
|
return components.includes("kai-file-upload") && components.includes("kai-attachments");
|
|
@@ -3512,9 +4691,20 @@ function sampleSeedComment(isMock, pad, decl) {
|
|
|
3512
4691
|
];
|
|
3513
4692
|
}
|
|
3514
4693
|
function componentTags(components, chatFill) {
|
|
4694
|
+
const block = isWorkspaceBlock(components);
|
|
4695
|
+
const slotted = slottedInChat(components);
|
|
4696
|
+
const slottedTags = new Set(slotted.map((s) => s.tag));
|
|
4697
|
+
const chatSlotted = block ? slotted.filter((s) => !BLOCK_TAGS.has(s.tag)) : slotted;
|
|
3515
4698
|
const companionTags = components.filter(
|
|
3516
|
-
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !
|
|
4699
|
+
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !ARTIFACT_SPLIT_TAGS.has(t) && !(block && BLOCK_TAGS.has(t)) && !slottedTags.has(t) && !(hasAttachments(components) && ATTACHMENT_TAGS.has(t))
|
|
3517
4700
|
);
|
|
4701
|
+
const chatBlock = (pad) => chatSlotted.length === 0 ? [`${pad}<kai-chat id="chat" suggestion-mode="submit" style="${chatFill}"></kai-chat>`] : [
|
|
4702
|
+
`${pad}<kai-chat id="chat" suggestion-mode="submit" style="${chatFill}">`,
|
|
4703
|
+
// wired: `htmlModule` below emits the property, the two listeners and
|
|
4704
|
+
// the thread map. This is the one target that does.
|
|
4705
|
+
...slottedChildMarkup(components, `${pad} `, { wired: true, ids: true }),
|
|
4706
|
+
`${pad}</kai-chat>`
|
|
4707
|
+
];
|
|
3518
4708
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
3519
4709
|
const hasStandaloneCompanions = companionTags.length > 0;
|
|
3520
4710
|
const lines = [];
|
|
@@ -3529,13 +4719,13 @@ function componentTags(components, chatFill) {
|
|
|
3529
4719
|
` </div>`
|
|
3530
4720
|
);
|
|
3531
4721
|
}
|
|
3532
|
-
if (
|
|
4722
|
+
if (isArtifactSplit(components)) {
|
|
3533
4723
|
lines.push(
|
|
3534
4724
|
` <!-- SCAF-14: workspace split — chat pane left, artifact preview right. -->`,
|
|
3535
4725
|
` <!-- kai-resizable needs kai-resizable-item children to render panels. -->`,
|
|
3536
4726
|
` <kai-resizable orientation="horizontal" style="display:block;width:100%;height:100%">`,
|
|
3537
4727
|
` <kai-resizable-item size="40%" min="240px">`,
|
|
3538
|
-
|
|
4728
|
+
...chatBlock(" "),
|
|
3539
4729
|
` </kai-resizable-item>`,
|
|
3540
4730
|
` <kai-resizable-item min="280px">`,
|
|
3541
4731
|
` <!-- Replace src with your artifact URL or set .files for multi-file preview. -->`,
|
|
@@ -3544,7 +4734,7 @@ function componentTags(components, chatFill) {
|
|
|
3544
4734
|
` </kai-resizable>`
|
|
3545
4735
|
);
|
|
3546
4736
|
} else {
|
|
3547
|
-
lines.push(
|
|
4737
|
+
lines.push(...chatBlock(" "));
|
|
3548
4738
|
}
|
|
3549
4739
|
if (hasEmbedded) {
|
|
3550
4740
|
lines.push(
|
|
@@ -3566,12 +4756,35 @@ function componentTags(components, chatFill) {
|
|
|
3566
4756
|
if (hasStandaloneCompanions) {
|
|
3567
4757
|
lines.push(` <!-- wire data props — see the component_reference MCP tool -->`);
|
|
3568
4758
|
}
|
|
4759
|
+
if (block) {
|
|
4760
|
+
return [
|
|
4761
|
+
` <!-- ── The workspace BLOCK: the shell + the rail + your thread records ── -->`,
|
|
4762
|
+
...htmlComment(
|
|
4763
|
+
[
|
|
4764
|
+
`<kai-workspace> is the chat-agnostic layout shell: header/start/main/`,
|
|
4765
|
+
`end/footer slots, aside resize + collapse, and a mobile drawer below`,
|
|
4766
|
+
`the drawer-below width. It knows nothing about chat.`
|
|
4767
|
+
],
|
|
4768
|
+
" "
|
|
4769
|
+
),
|
|
4770
|
+
` <kai-workspace drawer-below="720" style="display:block;width:100%;height:100%">`,
|
|
4771
|
+
...htmlComment([...BLOCK_RAIL_NOTE, `src/main.ts drives the rail.`], " "),
|
|
4772
|
+
` <kai-conversations id="conversations" slot="start" style="display:block;height:100%"></kai-conversations>`,
|
|
4773
|
+
` <!-- Unnamed children project into the shell's MAIN region. -->`,
|
|
4774
|
+
` <div style="display:flex;flex-direction:column;height:100%;min-height:0">`,
|
|
4775
|
+
...lines.map((l) => l === "" ? l : ` ${l}`),
|
|
4776
|
+
` </div>`,
|
|
4777
|
+
` </kai-workspace>`
|
|
4778
|
+
].join("\n");
|
|
4779
|
+
}
|
|
3569
4780
|
return lines.join("\n");
|
|
3570
4781
|
}
|
|
3571
4782
|
function htmlModule(ctx, components) {
|
|
3572
4783
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
3573
4784
|
const hasSources = components.includes("kai-sources");
|
|
3574
4785
|
const attachments = hasAttachments(components);
|
|
4786
|
+
const block = isWorkspaceBlock(components);
|
|
4787
|
+
const conversations = !block && slottedInChat(components).some((s) => s.tag === "kai-conversations");
|
|
3575
4788
|
const attachmentModuleLines = attachments ? [
|
|
3576
4789
|
...ATTACHMENT_WIRE_NOTE,
|
|
3577
4790
|
`//`,
|
|
@@ -3616,6 +4829,152 @@ function htmlModule(ctx, components) {
|
|
|
3616
4829
|
` sourcesEl.sources = sampleSources;`,
|
|
3617
4830
|
``
|
|
3618
4831
|
] : [];
|
|
4832
|
+
const conversationsSetupLines = conversations ? [
|
|
4833
|
+
` const conversationsEl = document.getElementById('conversations') as KaiConversationsElement;`,
|
|
4834
|
+
` // Same upgrade rule as <kai-chat>: 'conversations' is an array, so it is a`,
|
|
4835
|
+
` // PROPERTY, and a property set before the element upgrades is dropped on upgrade.`,
|
|
4836
|
+
` await customElements.whenDefined('kai-conversations');`,
|
|
4837
|
+
` // The row shape, DERIVED from the property it is assigned to — it cannot`,
|
|
4838
|
+
` // drift out of step with the element the way a hand-written copy would.`,
|
|
4839
|
+
` type Conversation = KaiConversationsElement['conversations'][number];`,
|
|
4840
|
+
` // Replace this list with your own. Your store owns titles, grouping and order;`,
|
|
4841
|
+
` // the rail renders what it is given, in the order it is given.`,
|
|
4842
|
+
` const now = new Date().toISOString();`,
|
|
4843
|
+
` let conversationRows: Conversation[] = [`,
|
|
4844
|
+
` { id: 'c1', title: 'New chat', scope: { type: 'document' }, messageCount: 0, lastMessageAt: now, updatedAt: now },`,
|
|
4845
|
+
` ];`,
|
|
4846
|
+
` let activeId = conversationRows[0].id;`,
|
|
4847
|
+
` // The threads you have open, keyed by conversation id. It starts EMPTY on`,
|
|
4848
|
+
` // purpose: seeding it would put words in the user's mouth. Load a thread`,
|
|
4849
|
+
` // from your backend in the select handler below and this fills itself.`,
|
|
4850
|
+
` const threads: Record<string, KaiChatElement['messages']> = {};`,
|
|
4851
|
+
` // A NEW array per write — the array reference is what notifies.`,
|
|
4852
|
+
` const showConversations = () => {`,
|
|
4853
|
+
` conversationsEl.conversations = [...conversationRows];`,
|
|
4854
|
+
` conversationsEl.activeId = activeId;`,
|
|
4855
|
+
` };`,
|
|
4856
|
+
` showConversations();`,
|
|
4857
|
+
``,
|
|
4858
|
+
` // The row's title, derived the way every chat app derives it: the first`,
|
|
4859
|
+
` // thing the user said, on one line. Swap in your own — a summariser, or`,
|
|
4860
|
+
` // whatever the user renamed the thread to — this is the fallback, not a`,
|
|
4861
|
+
` // policy. Returns undefined for an empty thread so the row keeps the`,
|
|
4862
|
+
` // placeholder it was created with rather than losing its label.`,
|
|
4863
|
+
` const TITLE_MAX = 48;`,
|
|
4864
|
+
` const titleFrom = (messages: KaiChatElement['messages']): string | undefined => {`,
|
|
4865
|
+
` const first = messages.find((message) => message.role === 'user');`,
|
|
4866
|
+
` // flatMap, not filter: a message can open with file parts, and this`,
|
|
4867
|
+
` // narrows the part union without needing a type predicate.`,
|
|
4868
|
+
` const text = (first?.parts ?? [])`,
|
|
4869
|
+
` .flatMap((part) => (part.type === 'text' ? [part.text] : []))`,
|
|
4870
|
+
` .join(' ')`,
|
|
4871
|
+
` .replace(/\\s+/g, ' ')`,
|
|
4872
|
+
` .trim();`,
|
|
4873
|
+
` if (!text) return undefined;`,
|
|
4874
|
+
` return text.length > TITLE_MAX ? \`\${text.slice(0, TITLE_MAX).trimEnd()}…\` : text;`,
|
|
4875
|
+
` };`,
|
|
4876
|
+
``,
|
|
4877
|
+
` // The row and the thread are two views of one conversation, and only the`,
|
|
4878
|
+
` // HOST keeps them in step: <kai-conversations> renders what it is handed and`,
|
|
4879
|
+
` // reports what was clicked, nothing more. Without this the rail reads`,
|
|
4880
|
+
` // "New chat / 0 messages / just now" forever, whatever the user does.`,
|
|
4881
|
+
` const syncActiveRow = () => {`,
|
|
4882
|
+
` const messages = chat.messages ?? [];`,
|
|
4883
|
+
` const at = new Date().toISOString();`,
|
|
4884
|
+
` // BOTH HALVES OF THE CONTRACT, and the rail is silently stale without`,
|
|
4885
|
+
` // either. A NEW ARRAY is what NOTIFIES — handing the same array back is a`,
|
|
4886
|
+
` // no-op even when a row inside it changed. A NEW OBJECT for the row that`,
|
|
4887
|
+
` // changed is what makes it VISIBLE, because the rail renders rows through`,
|
|
4888
|
+
` // a reference-keyed list and never re-renders a row whose identity held.`,
|
|
4889
|
+
` // So: map to a fresh row. Never \`row.messageCount = …\` — that mutation`,
|
|
4890
|
+
` // updates the data and leaves the screen showing the old numbers.`,
|
|
4891
|
+
` conversationRows = conversationRows.map((row) =>`,
|
|
4892
|
+
` row.id === activeId`,
|
|
4893
|
+
` ? {`,
|
|
4894
|
+
` ...row,`,
|
|
4895
|
+
` title: titleFrom(messages) ?? row.title,`,
|
|
4896
|
+
` messageCount: messages.length,`,
|
|
4897
|
+
` lastMessageAt: at,`,
|
|
4898
|
+
` updatedAt: at,`,
|
|
4899
|
+
` }`,
|
|
4900
|
+
` : row,`,
|
|
4901
|
+
` );`,
|
|
4902
|
+
` showConversations();`,
|
|
4903
|
+
` };`,
|
|
4904
|
+
``,
|
|
4905
|
+
` conversationsEl.addEventListener('kai-conversation-select', (event: Event) => {`,
|
|
4906
|
+
` const { id } = (event as CustomEvent<{ id: string }>).detail;`,
|
|
4907
|
+
` // Keep what is on screen before switching away from it.`,
|
|
4908
|
+
` threads[activeId] = chat.messages ?? [];`,
|
|
4909
|
+
` activeId = id;`,
|
|
4910
|
+
` // ...and load the one picked. Fetch it here if your store is remote.`,
|
|
4911
|
+
` chat.messages = [...(threads[id] ?? [])];`,
|
|
4912
|
+
` showConversations();`,
|
|
4913
|
+
` });`,
|
|
4914
|
+
` conversationsEl.addEventListener('kai-new-chat', () => {`,
|
|
4915
|
+
` // The event carries no detail by design: it IS the whole signal.`,
|
|
4916
|
+
` threads[activeId] = chat.messages ?? [];`,
|
|
4917
|
+
` const at = new Date().toISOString();`,
|
|
4918
|
+
` activeId = \`c\${conversationRows.length + 1}\`;`,
|
|
4919
|
+
` conversationRows = [`,
|
|
4920
|
+
` { id: activeId, title: 'New chat', scope: { type: 'document' }, messageCount: 0, lastMessageAt: at, updatedAt: at },`,
|
|
4921
|
+
` ...conversationRows,`,
|
|
4922
|
+
` ];`,
|
|
4923
|
+
` chat.messages = [];`,
|
|
4924
|
+
` showConversations();`,
|
|
4925
|
+
` });`,
|
|
4926
|
+
``
|
|
4927
|
+
] : [];
|
|
4928
|
+
const blockSetupLines = block ? [
|
|
4929
|
+
` // ── The block's state: your records + the kit's session/persistence helpers ─`,
|
|
4930
|
+
` const conversationsEl = document.getElementById('conversations') as KaiConversationsElement;`,
|
|
4931
|
+
` // Same upgrade rule as <kai-chat>: 'conversations' is an array, so it is a`,
|
|
4932
|
+
` // PROPERTY, and a property set before the element upgrades is dropped on upgrade.`,
|
|
4933
|
+
` await customElements.whenDefined('kai-conversations');`,
|
|
4934
|
+
``,
|
|
4935
|
+
` let threads: Thread[] = loadThreads();`,
|
|
4936
|
+
` // null means a DRAFT: "New chat" was pressed and nothing is stored yet. A`,
|
|
4937
|
+
` // record is created on its first turn, so the rail never fills with empties.`,
|
|
4938
|
+
` let activeId: string | null = threads[0]?.id ?? null;`,
|
|
4939
|
+
...BLOCK_SESSIONS_NOTE.map((l) => ` // ${l}`),
|
|
4940
|
+
` const sessions = createThreadSessions(() => showWorkspace());`,
|
|
4941
|
+
` // The save scheduler: the debounce/flush MECHANICS are the kit's; the delay,`,
|
|
4942
|
+
` // the save target and what a failed save means are YOURS (see saveThreads).`,
|
|
4943
|
+
` const saver = createSaveScheduler(saveThreads, { delayMs: 250 });`,
|
|
4944
|
+
` // Flush the pending write when the tab closes mid-debounce (or mid-stream).`,
|
|
4945
|
+
` window.addEventListener('beforeunload', () => saver.flush());`,
|
|
4946
|
+
` // Every write goes through this: schedule the save and re-project the views.`,
|
|
4947
|
+
` const setThreads = (updater: (prev: Thread[]) => Thread[]): void => {`,
|
|
4948
|
+
` threads = updater(threads);`,
|
|
4949
|
+
` saver.schedule(threads);`,
|
|
4950
|
+
` showWorkspace();`,
|
|
4951
|
+
` };`,
|
|
4952
|
+
``,
|
|
4953
|
+
` // Project the records onto the elements. NEW arrays per write — the array`,
|
|
4954
|
+
` // reference is what notifies (reactivity-two-halves; the changed objects`,
|
|
4955
|
+
` // inside come from the helpers' folds).`,
|
|
4956
|
+
` function showWorkspace(): void {`,
|
|
4957
|
+
` // The rail's row shape is a VIEW of your record — titles and counts, no`,
|
|
4958
|
+
` // message bodies. The rail renders the array in the order it is given.`,
|
|
4959
|
+
` conversationsEl.conversations = threads.map((t) => ({ id: t.id, title: t.title, messageCount: t.messages.length, updatedAt: t.updatedAt }));`,
|
|
4960
|
+
` conversationsEl.activeId = activeId ?? undefined;`,
|
|
4961
|
+
` chat.messages = [...(threads.find((t) => t.id === activeId)?.messages ?? [])];`,
|
|
4962
|
+
` chat.loading = activeId !== null && sessions.isStreaming(activeId);`,
|
|
4963
|
+
` }`,
|
|
4964
|
+
` showWorkspace();`,
|
|
4965
|
+
``,
|
|
4966
|
+
` conversationsEl.addEventListener('kai-conversation-select', (event: Event) => {`,
|
|
4967
|
+
` activeId = (event as CustomEvent<{ id: string }>).detail.id;`,
|
|
4968
|
+
` showWorkspace();`,
|
|
4969
|
+
` });`,
|
|
4970
|
+
` conversationsEl.addEventListener('kai-new-chat', () => {`,
|
|
4971
|
+
` // The event carries no detail by design: it IS the whole signal. A DRAFT,`,
|
|
4972
|
+
` // not a record — see activeId above.`,
|
|
4973
|
+
` activeId = null;`,
|
|
4974
|
+
` showWorkspace();`,
|
|
4975
|
+
` });`,
|
|
4976
|
+
``
|
|
4977
|
+
] : [];
|
|
3619
4978
|
const modelLines = ctx.defaultModel ? [
|
|
3620
4979
|
`// SCAF-8: change this model id to another id THIS PROVIDER accepts.`,
|
|
3621
4980
|
`const model = '${ctx.defaultModel}';`,
|
|
@@ -3633,9 +4992,13 @@ function htmlModule(ctx, components) {
|
|
|
3633
4992
|
const elementTypes = [
|
|
3634
4993
|
"KaiChatElement",
|
|
3635
4994
|
...hasSources ? ["KaiSourcesElement"] : [],
|
|
3636
|
-
...attachments ? ["KaiAttachmentsElement"] : []
|
|
4995
|
+
...attachments ? ["KaiAttachmentsElement"] : [],
|
|
4996
|
+
// Same rule again: only when the rail is really on this surface. It types the
|
|
4997
|
+
// property assignments AND supplies the row type, so it is used twice over.
|
|
4998
|
+
// The block drives the same element from its own thread store.
|
|
4999
|
+
...conversations || block ? ["KaiConversationsElement"] : []
|
|
3637
5000
|
].join(", ");
|
|
3638
|
-
const annotatesChatMessage = !ctx.emitToolLoop;
|
|
5001
|
+
const annotatesChatMessage = !ctx.emitToolLoop || block;
|
|
3639
5002
|
const head = [
|
|
3640
5003
|
`// src/main.ts — the page's logic, in a module YOUR build type-checks.`,
|
|
3641
5004
|
`//`,
|
|
@@ -3654,13 +5017,15 @@ function htmlModule(ctx, components) {
|
|
|
3654
5017
|
cards: ctx.emitCards,
|
|
3655
5018
|
cardTools: ctx.cardProvider !== null,
|
|
3656
5019
|
mock: ctx.isMock,
|
|
3657
|
-
attachments
|
|
5020
|
+
attachments,
|
|
5021
|
+
workspaceBlock: block
|
|
3658
5022
|
}),
|
|
3659
5023
|
`import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
3660
5024
|
``,
|
|
3661
5025
|
...ctx.isMock ? [...mockResponderInit(), ``] : [],
|
|
3662
5026
|
...modelLines,
|
|
3663
5027
|
...attachmentModuleLines,
|
|
5028
|
+
...block ? [...blockThreadStoreLines(""), ``] : [],
|
|
3664
5029
|
...cardsLines,
|
|
3665
5030
|
...toolsLines,
|
|
3666
5031
|
...runnerLines,
|
|
@@ -3677,7 +5042,9 @@ function htmlModule(ctx, components) {
|
|
|
3677
5042
|
...cardPropLines,
|
|
3678
5043
|
...seedLines,
|
|
3679
5044
|
...sourcesSetupLines,
|
|
3680
|
-
...attachmentSetupLines
|
|
5045
|
+
...attachmentSetupLines,
|
|
5046
|
+
...conversationsSetupLines,
|
|
5047
|
+
...blockSetupLines
|
|
3681
5048
|
];
|
|
3682
5049
|
const listenerOpen = [
|
|
3683
5050
|
` chat.addEventListener('kai-submit', async (event: Event) => {`,
|
|
@@ -3690,33 +5057,62 @@ function htmlModule(ctx, components) {
|
|
|
3690
5057
|
`// A <script type="module"> is deferred, so the DOM is already parsed here.`,
|
|
3691
5058
|
`void init();`
|
|
3692
5059
|
];
|
|
5060
|
+
const blockPrelude = block ? [
|
|
5061
|
+
...BLOCK_TARGET_ID_NOTE.map((l) => `// ${l}`),
|
|
5062
|
+
`const targetId = activeId ?? crypto.randomUUID();`,
|
|
5063
|
+
`// The record is born on its first turn — with YOUR title policy.`,
|
|
5064
|
+
`setThreads((prev) => prev.some((t) => t.id === targetId)`,
|
|
5065
|
+
` ? prev`,
|
|
5066
|
+
` : [{ id: targetId, title: deriveTitle(value), updatedAt: new Date().toISOString(), messages: [] }, ...prev]);`,
|
|
5067
|
+
`activeId = targetId;`,
|
|
5068
|
+
...BLOCK_BIND_NOTE.map((l) => `// ${l}`),
|
|
5069
|
+
`const setMessages = bindThreadMessages<Thread>(setThreads, targetId, {`,
|
|
5070
|
+
` touch: (t) => ({ ...t, updatedAt: new Date().toISOString() }),`,
|
|
5071
|
+
`});`,
|
|
5072
|
+
`const controller = sessions.begin(targetId);`,
|
|
5073
|
+
`showWorkspace();`
|
|
5074
|
+
] : [];
|
|
3693
5075
|
return [
|
|
3694
5076
|
...head,
|
|
3695
5077
|
` // messages is a JS PROPERTY (objects can't be HTML attributes)`,
|
|
3696
5078
|
...listenerOpen,
|
|
3697
5079
|
realStreamBody({
|
|
3698
5080
|
pad: " ",
|
|
3699
|
-
read: "chat.messages",
|
|
3700
|
-
commitSet: (expr) => `chat.messages = ${expr};`,
|
|
3701
|
-
setterAdapter: "(fn) => { chat.messages = fn(chat.messages); }",
|
|
3702
|
-
setLoading: (v2) => `chat.loading = ${v2};`,
|
|
5081
|
+
read: block ? `(threads.find((t) => t.id === targetId)?.messages ?? [])` : "chat.messages",
|
|
5082
|
+
commitSet: block ? (expr) => `setMessages(() => ${expr});` : (expr) => `chat.messages = ${expr};`,
|
|
5083
|
+
setterAdapter: block ? "setMessages" : "(fn) => { chat.messages = fn(chat.messages); }",
|
|
5084
|
+
setLoading: block ? (v2) => v2 === "true" ? `// loading for THIS thread now derives from the sessions map (showWorkspace).` : `sessions.end(targetId, controller); showWorkspace();` : (v2) => `chat.loading = ${v2};`,
|
|
3703
5085
|
bodyPayload: realBodyPayload({ defaultModel: ctx.defaultModel, tools: ctx.emitTools }),
|
|
3704
5086
|
strictRoles: true,
|
|
3705
5087
|
toolLoop: ctx.emitToolLoop,
|
|
3706
5088
|
cards: ctx.emitCards,
|
|
3707
|
-
thread: liveThreadBinding(
|
|
5089
|
+
thread: block ? blockLiveThread(`(threads.find((t) => t.id === targetId)?.messages ?? [])`) : liveThreadBinding(
|
|
3708
5090
|
"chat.messages",
|
|
3709
5091
|
"(fn) => { chat.messages = fn(chat.messages); }",
|
|
3710
5092
|
"chat.messages ?? []"
|
|
3711
5093
|
),
|
|
3712
5094
|
mock: ctx.isMock,
|
|
5095
|
+
...block ? { fetchSignal: "controller.signal", afterValue: blockPrelude } : {},
|
|
5096
|
+
// ONE call site, in the `finally`, and deliberately not also on submit. The
|
|
5097
|
+
// finally sees the WHOLE turn — the user's message and the assistant's — so
|
|
5098
|
+
// one call cannot leave the row disagreeing with the thread, and it runs on
|
|
5099
|
+
// the error path too, where the user's message is on the thread regardless.
|
|
5100
|
+
// The cost is that the title appears when the reply settles rather than the
|
|
5101
|
+
// instant the user hits send; call it right after `chat.messages = history`
|
|
5102
|
+
// as well if you want it sooner.
|
|
5103
|
+
afterTurn: conversations ? [`syncActiveRow(); // keep the rail's row in step with the thread`] : [],
|
|
3713
5104
|
...attachments ? {
|
|
3714
5105
|
filesExpr: "files",
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
5106
|
+
// The attachment capture must read the staging list before anything
|
|
5107
|
+
// clears it, so it precedes the block's prelude.
|
|
5108
|
+
afterValue: [
|
|
5109
|
+
...attachmentTurnLines({
|
|
5110
|
+
stagedExpr: "staged",
|
|
5111
|
+
clear: ["staged = [];", "showStaged();"],
|
|
5112
|
+
fromSubmitEvent: true
|
|
5113
|
+
}),
|
|
5114
|
+
...blockPrelude
|
|
5115
|
+
]
|
|
3720
5116
|
} : {}
|
|
3721
5117
|
}),
|
|
3722
5118
|
...footer
|
|
@@ -3755,14 +5151,17 @@ function toPascalCase(tag) {
|
|
|
3755
5151
|
function renderJsx(components, ctx, framework) {
|
|
3756
5152
|
const { p, emptyHint, suggestions, isMock, defaultModel, emitTools, emitToolLoop } = ctx;
|
|
3757
5153
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
3758
|
-
const workspace =
|
|
3759
|
-
const
|
|
5154
|
+
const workspace = isArtifactSplit(components);
|
|
5155
|
+
const block = isWorkspaceBlock(components);
|
|
5156
|
+
const renderableTags = [
|
|
5157
|
+
.../* @__PURE__ */ new Set([ALWAYS_EMITTED_TAG, ...components.filter((t) => !MESSAGE_EMBEDDED_TAGS.has(t))])
|
|
5158
|
+
];
|
|
3760
5159
|
const importTags = workspace ? [.../* @__PURE__ */ new Set([...renderableTags.filter((t) => t !== "kai-resizable"), "kai-resizable", "kai-resizable-item"])] : renderableTags;
|
|
3761
5160
|
const wrapperNames = importTags.map(toPascalCase);
|
|
3762
5161
|
const importList = wrapperNames.join(", ");
|
|
3763
5162
|
const attachments = hasAttachments(components);
|
|
3764
5163
|
const standaloneCompanionTags = components.filter(
|
|
3765
|
-
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !
|
|
5164
|
+
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !ARTIFACT_SPLIT_TAGS.has(t) && !(block && BLOCK_TAGS.has(t)) && !(attachments && ATTACHMENT_TAGS.has(t))
|
|
3766
5165
|
);
|
|
3767
5166
|
const companionJsxLines = [];
|
|
3768
5167
|
if (hasEmbedded) {
|
|
@@ -3777,6 +5176,8 @@ function renderJsx(components, ctx, framework) {
|
|
|
3777
5176
|
` {/* Replace sampleSources with your real data. */}`,
|
|
3778
5177
|
` <Sources sources={sampleSources} />`
|
|
3779
5178
|
);
|
|
5179
|
+
} else if (slotPlacementFor(t)) {
|
|
5180
|
+
companionJsxLines.push(...railSiblingNote(t, slotPlacementFor(t)), ` <${toPascalCase(t)} />`);
|
|
3780
5181
|
} else {
|
|
3781
5182
|
companionJsxLines.push(` {/* wire data props — see the component_reference MCP tool */}`);
|
|
3782
5183
|
companionJsxLines.push(` <${toPascalCase(t)} />`);
|
|
@@ -3819,13 +5220,54 @@ function renderJsx(components, ctx, framework) {
|
|
|
3819
5220
|
} : {};
|
|
3820
5221
|
const submitParamType = attachments ? "CustomEvent<{ value: string; attachments: AttachmentData[] }>" : "CustomEvent<{ value: string }>";
|
|
3821
5222
|
const mockInit = isMock ? mockResponderInit() : [];
|
|
3822
|
-
const sampleMessagesInit = [
|
|
5223
|
+
const sampleMessagesInit = block ? [
|
|
5224
|
+
` // ── The block's state: your records + the kit's session/persistence helpers ─`,
|
|
5225
|
+
` const [threads, setThreads] = useState<Thread[]>(loadThreads);`,
|
|
5226
|
+
` // null means a DRAFT: "New chat" was pressed and nothing is stored yet. A`,
|
|
5227
|
+
` // record is created on its first turn, so the rail never fills with empties.`,
|
|
5228
|
+
` const [activeId, setActiveId] = useState<string | null>(threads[0]?.id ?? null);`,
|
|
5229
|
+
` // One id per in-flight reply — a stream keeps running when you switch away,`,
|
|
5230
|
+
` // so this cannot be a single boolean.`,
|
|
5231
|
+
` const [streamingIds, setStreamingIds] = useState<string[]>([]);`,
|
|
5232
|
+
...BLOCK_SESSIONS_NOTE.map((l) => ` // ${l}`),
|
|
5233
|
+
` const [sessions] = useState(() => createThreadSessions(setStreamingIds));`,
|
|
5234
|
+
` // The save scheduler: the debounce/flush MECHANICS are the kit's; the delay,`,
|
|
5235
|
+
` // the save target and what a failed save means are YOURS (see saveThreads).`,
|
|
5236
|
+
` const [saver] = useState(() => createSaveScheduler(saveThreads, { delayMs: 250 }));`,
|
|
5237
|
+
` useEffect(() => { saver.schedule(threads); }, [saver, threads]);`,
|
|
5238
|
+
` useEffect(() => {`,
|
|
5239
|
+
` // Flush the pending write when the tab closes mid-debounce (or mid-stream).`,
|
|
5240
|
+
` const flush = () => saver.flush();`,
|
|
5241
|
+
` window.addEventListener('beforeunload', flush);`,
|
|
5242
|
+
` return () => window.removeEventListener('beforeunload', flush);`,
|
|
5243
|
+
` }, [saver]);`,
|
|
5244
|
+
``,
|
|
5245
|
+
` const active = threads.find((t) => t.id === activeId) ?? null;`,
|
|
5246
|
+
` const messages = active?.messages ?? NO_MESSAGES;`,
|
|
5247
|
+
` const loading = activeId !== null && streamingIds.includes(activeId);`,
|
|
5248
|
+
` // The rail's row shape is a VIEW of your record — titles and counts, no`,
|
|
5249
|
+
` // message bodies. Newest-first is your ordering; the rail renders the array`,
|
|
5250
|
+
` // in the order it is given.`,
|
|
5251
|
+
` const rows = threads.map((t) => ({ id: t.id, title: t.title, messageCount: t.messages.length, updatedAt: t.updatedAt }));`
|
|
5252
|
+
].join("\n") : [
|
|
3823
5253
|
...hasEmbedded ? sampleSeedComment(isMock, " ", (literal) => [
|
|
3824
5254
|
`const sampleMessages: ChatMessage[] = [${literal}];`,
|
|
3825
5255
|
`const [messages, setMessages] = useState<ChatMessage[]>(sampleMessages);`
|
|
3826
5256
|
]) : [],
|
|
3827
5257
|
` const [messages, setMessages] = useState<ChatMessage[]>([]);`
|
|
3828
5258
|
].join("\n");
|
|
5259
|
+
const blockModuleInit = block ? [
|
|
5260
|
+
// One joined entry so the JSX branches' blank-line filter cannot squash
|
|
5261
|
+
// the store's own paragraph breaks.
|
|
5262
|
+
[
|
|
5263
|
+
`/** A stable reference for the empty thread. \`messages\` is diffed by`,
|
|
5264
|
+
` * reference, so a fresh \`[]\` every render would re-notify for nothing. */`,
|
|
5265
|
+
`const NO_MESSAGES: ChatMessage[] = [];`,
|
|
5266
|
+
``,
|
|
5267
|
+
...blockThreadStoreLines("")
|
|
5268
|
+
].join("\n")
|
|
5269
|
+
] : [];
|
|
5270
|
+
const loadingInit = block ? [] : [` const [loading, setLoading] = useState(false);`];
|
|
3829
5271
|
const sampleSourcesInit = standaloneCompanionTags.includes("kai-sources") ? [
|
|
3830
5272
|
` // Replace sampleSources with your real source data.`,
|
|
3831
5273
|
` const sampleSources = [`,
|
|
@@ -3840,21 +5282,109 @@ function renderJsx(components, ctx, framework) {
|
|
|
3840
5282
|
const cardsInit = ctx.emitCards ? cardRegistryLines("") : [];
|
|
3841
5283
|
const cardProps = (pad) => ctx.emitCards ? [`${pad}cardTypes={cards.tags}`, `${pad}cardSchemas={cards.validationSchemas}`] : [];
|
|
3842
5284
|
const cardPropsNote = (pad) => ctx.emitCards ? jsxComment(CARD_PROP_COMMENT, pad) : [];
|
|
5285
|
+
const blockPrelude = block ? [
|
|
5286
|
+
...BLOCK_TARGET_ID_NOTE.map((l) => `// ${l}`),
|
|
5287
|
+
`const targetId = activeId ?? crypto.randomUUID();`,
|
|
5288
|
+
`// The record is born on its first turn — with YOUR title policy.`,
|
|
5289
|
+
`setThreads((prev) => prev.some((t) => t.id === targetId)`,
|
|
5290
|
+
` ? prev`,
|
|
5291
|
+
` : [{ id: targetId, title: deriveTitle(value), updatedAt: new Date().toISOString(), messages: [] }, ...prev]);`,
|
|
5292
|
+
`setActiveId(targetId);`,
|
|
5293
|
+
...BLOCK_BIND_NOTE.map((l) => `// ${l}`),
|
|
5294
|
+
`const setMessages = bindThreadMessages<Thread>(setThreads, targetId, {`,
|
|
5295
|
+
` touch: (t) => ({ ...t, updatedAt: new Date().toISOString() }),`,
|
|
5296
|
+
`});`,
|
|
5297
|
+
`const controller = sessions.begin(targetId);`
|
|
5298
|
+
] : [];
|
|
5299
|
+
const blockReactThread = {
|
|
5300
|
+
open: ({ pad, userMessage }) => [
|
|
5301
|
+
`${pad}// THE TURN OWNS THE THREAD (same rule as the single-thread scaffold): the`,
|
|
5302
|
+
`${pad}// loop below re-encodes it every round, and React state cannot be read`,
|
|
5303
|
+
`${pad}// back mid-turn. setMessages (the id-bound sink) just projects it.`,
|
|
5304
|
+
`${pad}let thread: ChatMessage[] = [...(threads.find((t) => t.id === targetId)?.messages ?? []), ${userMessage}];`,
|
|
5305
|
+
`${pad}const set: SetMessages = (fn) => { thread = fn(thread); setMessages(() => thread); };`,
|
|
5306
|
+
`${pad}setMessages(() => thread);`
|
|
5307
|
+
],
|
|
5308
|
+
live: "thread",
|
|
5309
|
+
setter: "set"
|
|
5310
|
+
};
|
|
3843
5311
|
const onSubmitBody = realStreamBody({
|
|
3844
5312
|
pad: " ",
|
|
3845
|
-
read: "messages",
|
|
3846
|
-
commitSet: (expr) => `setMessages(${expr});`,
|
|
3847
|
-
// useState's setter IS a SetMessages: both are (updater) => void
|
|
5313
|
+
read: block ? `(threads.find((t) => t.id === targetId)?.messages ?? [])` : "messages",
|
|
5314
|
+
commitSet: block ? (expr) => `setMessages(() => ${expr});` : (expr) => `setMessages(${expr});`,
|
|
5315
|
+
// useState's setter IS a SetMessages: both are (updater) => void — and in
|
|
5316
|
+
// block mode the id-bound sink from bindThreadMessages is one too.
|
|
3848
5317
|
setterAdapter: "setMessages",
|
|
3849
|
-
setLoading: (v2) => `setLoading(${v2});`,
|
|
5318
|
+
setLoading: block ? (v2) => v2 === "true" ? `// loading for THIS thread now derives from the sessions map (streamingIds).` : `sessions.end(targetId, controller);` : (v2) => `setLoading(${v2});`,
|
|
3850
5319
|
bodyPayload: realBodyPayload({ defaultModel, tools: emitTools }),
|
|
3851
5320
|
strictRoles: true,
|
|
3852
5321
|
toolLoop: emitToolLoop,
|
|
3853
5322
|
cards: ctx.emitCards,
|
|
3854
|
-
thread: REACT_THREAD,
|
|
5323
|
+
thread: block ? blockReactThread : REACT_THREAD,
|
|
3855
5324
|
mock: isMock,
|
|
3856
|
-
...
|
|
5325
|
+
...block ? { fetchSignal: "controller.signal" } : {},
|
|
5326
|
+
...attachmentSubmitOpts,
|
|
5327
|
+
...block ? {
|
|
5328
|
+
afterValue: [
|
|
5329
|
+
...attachmentSubmitOpts.afterValue ?? [],
|
|
5330
|
+
...blockPrelude
|
|
5331
|
+
]
|
|
5332
|
+
} : {}
|
|
3857
5333
|
});
|
|
5334
|
+
const chatJsx = (pad) => [
|
|
5335
|
+
...cardPropsNote(pad),
|
|
5336
|
+
`${pad}<Chat`,
|
|
5337
|
+
`${pad} messages={messages}`,
|
|
5338
|
+
`${pad} loading={loading}`,
|
|
5339
|
+
`${pad} suggestions={suggestions}`,
|
|
5340
|
+
`${pad} suggestionMode="submit"`,
|
|
5341
|
+
...cardProps(`${pad} `),
|
|
5342
|
+
`${pad} onSubmit={onSubmit}`,
|
|
5343
|
+
`${pad} style={{ ${jsxStyle(p.chatFill)} }}`,
|
|
5344
|
+
`${pad}/>`
|
|
5345
|
+
];
|
|
5346
|
+
const splitJsx = [
|
|
5347
|
+
` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
|
|
5348
|
+
` {/* Resizable needs ResizableItem children to render panels. */}`,
|
|
5349
|
+
` <Resizable orientation="horizontal" style={{ display: 'block', width: '100%', height: '100%' }}>`,
|
|
5350
|
+
` <ResizableItem size="40%" min="240px">`,
|
|
5351
|
+
...chatJsx(" "),
|
|
5352
|
+
` </ResizableItem>`,
|
|
5353
|
+
` <ResizableItem min="280px">`,
|
|
5354
|
+
` {/* Replace src + files with your real artifact data (files is required: array/object props are never optional attributes on a kai-* element). */}`,
|
|
5355
|
+
` <Artifact src="https://example.com" files={[{ path: 'index.html', url: 'https://example.com' }]} style={{ width: '100%', height: '100%' }} />`,
|
|
5356
|
+
` </ResizableItem>`,
|
|
5357
|
+
` </Resizable>`
|
|
5358
|
+
];
|
|
5359
|
+
const mainJsx = [
|
|
5360
|
+
...attachmentJsx ? [attachmentJsx] : [],
|
|
5361
|
+
...workspace ? [...splitJsx, companions] : [...chatJsx(" "), companions]
|
|
5362
|
+
];
|
|
5363
|
+
const surfaceJsx = block ? [
|
|
5364
|
+
` {/* ── The workspace BLOCK: the shell + the rail + your thread records ── */}`,
|
|
5365
|
+
` {/* <Workspace> is the chat-agnostic layout shell (header/start/main/end/`,
|
|
5366
|
+
` footer slots, aside resize + collapse, a mobile drawer below`,
|
|
5367
|
+
` drawerBelow px). It knows nothing about chat. */}`,
|
|
5368
|
+
` <Workspace drawerBelow={720} style={{ display: 'block', width: '100%', height: '100%' }}>`,
|
|
5369
|
+
...jsxComment(BLOCK_RAIL_NOTE, " "),
|
|
5370
|
+
` {/* The React wrappers forward no slot prop, so a plain div carries the`,
|
|
5371
|
+
` slot attribute into the shell's start aside. */}`,
|
|
5372
|
+
` <div slot="start" style={{ height: '100%' }}>`,
|
|
5373
|
+
` {/* New chat = a DRAFT (activeId null): the record appears on the first turn. */}`,
|
|
5374
|
+
` <Conversations`,
|
|
5375
|
+
` conversations={rows}`,
|
|
5376
|
+
` activeId={activeId ?? undefined}`,
|
|
5377
|
+
` onConversationSelect={(e) => setActiveId(e.detail.id)}`,
|
|
5378
|
+
` onNewChat={() => setActiveId(null)}`,
|
|
5379
|
+
` style={{ display: 'block', height: '100%' }}`,
|
|
5380
|
+
` />`,
|
|
5381
|
+
` </div>`,
|
|
5382
|
+
` {/* Unnamed children project into the shell's MAIN region. */}`,
|
|
5383
|
+
` <div style={{ display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 }}>`,
|
|
5384
|
+
...indentEmitted(mainJsx, " "),
|
|
5385
|
+
` </div>`,
|
|
5386
|
+
` </Workspace>`
|
|
5387
|
+
] : mainJsx;
|
|
3858
5388
|
const useClientDirective = framework === "next" ? [`'use client';`, ``] : [];
|
|
3859
5389
|
if (framework === "next") {
|
|
3860
5390
|
const dynamicImports = wrapperNames.map(
|
|
@@ -3864,7 +5394,7 @@ function renderJsx(components, ctx, framework) {
|
|
|
3864
5394
|
return [
|
|
3865
5395
|
// 'use client' must be the very first line for Next.js App Router.
|
|
3866
5396
|
...useClientDirective,
|
|
3867
|
-
`import { useState } from 'react';`,
|
|
5397
|
+
`import { ${block ? "useEffect, useState" : "useState"} } from 'react';`,
|
|
3868
5398
|
`import dynamic from 'next/dynamic';`,
|
|
3869
5399
|
// The adapter is pure parsing + pure state; both entries are SSR-import-safe,
|
|
3870
5400
|
// so they stay static imports even though the ELEMENTS have to be dynamic.
|
|
@@ -3875,7 +5405,8 @@ function renderJsx(components, ctx, framework) {
|
|
|
3875
5405
|
cards: ctx.emitCards,
|
|
3876
5406
|
cardTools: ctx.cardProvider !== null,
|
|
3877
5407
|
mock: isMock,
|
|
3878
|
-
attachments
|
|
5408
|
+
attachments,
|
|
5409
|
+
workspaceBlock: block
|
|
3879
5410
|
}),
|
|
3880
5411
|
`import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
3881
5412
|
`// <kai-*> are client-only custom elements (the server has no customElements`,
|
|
@@ -3889,10 +5420,11 @@ function renderJsx(components, ctx, framework) {
|
|
|
3889
5420
|
...mockInit,
|
|
3890
5421
|
``,
|
|
3891
5422
|
...attachmentModuleInit,
|
|
5423
|
+
...blockModuleInit,
|
|
3892
5424
|
...cardsInit,
|
|
3893
5425
|
`export default function App() {`,
|
|
3894
5426
|
sampleMessagesInit,
|
|
3895
|
-
|
|
5427
|
+
...loadingInit,
|
|
3896
5428
|
` const suggestions = ${jsArray(suggestions)};`,
|
|
3897
5429
|
...attachmentStateInit ? [attachmentStateInit] : [],
|
|
3898
5430
|
...sampleSourcesInit ? [sampleSourcesInit] : [],
|
|
@@ -3906,46 +5438,7 @@ function renderJsx(components, ctx, framework) {
|
|
|
3906
5438
|
``,
|
|
3907
5439
|
` return (`,
|
|
3908
5440
|
` <div style={{ ${jsxStyle(p.style)} }}>`,
|
|
3909
|
-
...
|
|
3910
|
-
...workspace ? [
|
|
3911
|
-
` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
|
|
3912
|
-
` {/* Resizable needs ResizableItem children to render panels. */}`,
|
|
3913
|
-
` <Resizable orientation="horizontal" style={{ display: 'block', width: '100%', height: '100%' }}>`,
|
|
3914
|
-
` <ResizableItem size="40%" min="240px">`,
|
|
3915
|
-
...cardPropsNote(" "),
|
|
3916
|
-
` <Chat`,
|
|
3917
|
-
` messages={messages}`,
|
|
3918
|
-
` loading={loading}`,
|
|
3919
|
-
` suggestions={suggestions}`,
|
|
3920
|
-
` suggestionMode="submit"`,
|
|
3921
|
-
...cardProps(" "),
|
|
3922
|
-
` onSubmit={onSubmit}`,
|
|
3923
|
-
` style={{ ${jsxStyle(p.chatFill)} }}`,
|
|
3924
|
-
` />`,
|
|
3925
|
-
` </ResizableItem>`,
|
|
3926
|
-
` <ResizableItem min="280px">`,
|
|
3927
|
-
` {/* Replace src + files with your real artifact data (files is required: array/object props are never optional attributes on a kai-* element). */}`,
|
|
3928
|
-
` <Artifact src="https://example.com" files={[{ path: 'index.html', url: 'https://example.com' }]} style={{ width: '100%', height: '100%' }} />`,
|
|
3929
|
-
` </ResizableItem>`,
|
|
3930
|
-
` </Resizable>`,
|
|
3931
|
-
// Standalone companions are siblings of the SPLIT, not of the chat:
|
|
3932
|
-
// the split owns chat + artifact, and a sources panel or a voice input
|
|
3933
|
-
// belongs beside it. Dropping them here is what the workspace branch
|
|
3934
|
-
// used to do — see WORKSPACE_STRUCTURAL_TAGS.
|
|
3935
|
-
companions
|
|
3936
|
-
] : [
|
|
3937
|
-
...cardPropsNote(" "),
|
|
3938
|
-
` <Chat`,
|
|
3939
|
-
` messages={messages}`,
|
|
3940
|
-
` loading={loading}`,
|
|
3941
|
-
` suggestions={suggestions}`,
|
|
3942
|
-
` suggestionMode="submit"`,
|
|
3943
|
-
...cardProps(" "),
|
|
3944
|
-
` onSubmit={onSubmit}`,
|
|
3945
|
-
` style={{ ${jsxStyle(p.chatFill)} }}`,
|
|
3946
|
-
` />`,
|
|
3947
|
-
companions
|
|
3948
|
-
],
|
|
5441
|
+
...surfaceJsx,
|
|
3949
5442
|
` </div>`,
|
|
3950
5443
|
` );`,
|
|
3951
5444
|
`}`
|
|
@@ -3958,7 +5451,7 @@ function renderJsx(components, ctx, framework) {
|
|
|
3958
5451
|
// (1) REQUIRED: registers <kai-*> — the react wrappers do NOT auto-register.
|
|
3959
5452
|
// Must come BEFORE importing the wrappers, or <kai-chat> renders empty.
|
|
3960
5453
|
`import '@kitn.ai/ui/elements'; // registers <kai-*> — required, must come first`,
|
|
3961
|
-
`import { useState } from 'react';`,
|
|
5454
|
+
`import { ${block ? "useEffect, useState" : "useState"} } from 'react';`,
|
|
3962
5455
|
`import { ${importList} } from '@kitn.ai/ui/react';`,
|
|
3963
5456
|
...wireImportLines({
|
|
3964
5457
|
typed: true,
|
|
@@ -3967,7 +5460,8 @@ function renderJsx(components, ctx, framework) {
|
|
|
3967
5460
|
cards: ctx.emitCards,
|
|
3968
5461
|
cardTools: ctx.cardProvider !== null,
|
|
3969
5462
|
mock: isMock,
|
|
3970
|
-
attachments
|
|
5463
|
+
attachments,
|
|
5464
|
+
workspaceBlock: block
|
|
3971
5465
|
}),
|
|
3972
5466
|
`import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
3973
5467
|
``,
|
|
@@ -3977,10 +5471,11 @@ function renderJsx(components, ctx, framework) {
|
|
|
3977
5471
|
...mockInit,
|
|
3978
5472
|
``,
|
|
3979
5473
|
...attachmentModuleInit,
|
|
5474
|
+
...blockModuleInit,
|
|
3980
5475
|
...cardsInit,
|
|
3981
5476
|
`export default function App() {`,
|
|
3982
5477
|
sampleMessagesInit,
|
|
3983
|
-
|
|
5478
|
+
...loadingInit,
|
|
3984
5479
|
` const suggestions = ${jsArray(suggestions)};`,
|
|
3985
5480
|
...attachmentStateInit ? [attachmentStateInit] : [],
|
|
3986
5481
|
...sampleSourcesInit ? [sampleSourcesInit] : [],
|
|
@@ -3994,43 +5489,7 @@ function renderJsx(components, ctx, framework) {
|
|
|
3994
5489
|
``,
|
|
3995
5490
|
` return (`,
|
|
3996
5491
|
` <div style={{ ${jsxStyle(p.style)} }}>`,
|
|
3997
|
-
...
|
|
3998
|
-
...workspace ? [
|
|
3999
|
-
` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
|
|
4000
|
-
` {/* Resizable needs ResizableItem children to render panels. */}`,
|
|
4001
|
-
` <Resizable orientation="horizontal" style={{ display: 'block', width: '100%', height: '100%' }}>`,
|
|
4002
|
-
` <ResizableItem size="40%" min="240px">`,
|
|
4003
|
-
...cardPropsNote(" "),
|
|
4004
|
-
` <Chat`,
|
|
4005
|
-
` messages={messages}`,
|
|
4006
|
-
` loading={loading}`,
|
|
4007
|
-
` suggestions={suggestions}`,
|
|
4008
|
-
` suggestionMode="submit"`,
|
|
4009
|
-
...cardProps(" "),
|
|
4010
|
-
` onSubmit={onSubmit}`,
|
|
4011
|
-
` style={{ ${jsxStyle(p.chatFill)} }}`,
|
|
4012
|
-
` />`,
|
|
4013
|
-
` </ResizableItem>`,
|
|
4014
|
-
` <ResizableItem min="280px">`,
|
|
4015
|
-
` {/* Replace src + files with your real artifact data (files is required: array/object props are never optional attributes on a kai-* element). */}`,
|
|
4016
|
-
` <Artifact src="https://example.com" files={[{ path: 'index.html', url: 'https://example.com' }]} style={{ width: '100%', height: '100%' }} />`,
|
|
4017
|
-
` </ResizableItem>`,
|
|
4018
|
-
` </Resizable>`,
|
|
4019
|
-
// Siblings of the SPLIT — see the same line in the other JSX branch.
|
|
4020
|
-
companions
|
|
4021
|
-
] : [
|
|
4022
|
-
...cardPropsNote(" "),
|
|
4023
|
-
` <Chat`,
|
|
4024
|
-
` messages={messages}`,
|
|
4025
|
-
` loading={loading}`,
|
|
4026
|
-
` suggestions={suggestions}`,
|
|
4027
|
-
` suggestionMode="submit"`,
|
|
4028
|
-
...cardProps(" "),
|
|
4029
|
-
` onSubmit={onSubmit}`,
|
|
4030
|
-
` style={{ ${jsxStyle(p.chatFill)} }}`,
|
|
4031
|
-
` />`,
|
|
4032
|
-
companions
|
|
4033
|
-
],
|
|
5492
|
+
...surfaceJsx,
|
|
4034
5493
|
` </div>`,
|
|
4035
5494
|
` );`,
|
|
4036
5495
|
`}`
|
|
@@ -4038,10 +5497,13 @@ function renderJsx(components, ctx, framework) {
|
|
|
4038
5497
|
}
|
|
4039
5498
|
function renderVue(components, ctx) {
|
|
4040
5499
|
const { p, emptyHint, suggestions, isMock, defaultModel, emitTools, emitToolLoop } = ctx;
|
|
4041
|
-
const workspace =
|
|
5500
|
+
const workspace = isArtifactSplit(components);
|
|
5501
|
+
const block = isWorkspaceBlock(components);
|
|
4042
5502
|
const attachments = hasAttachments(components);
|
|
5503
|
+
const slottedTags = new Set(slottedInChat(components).map((s) => s.tag));
|
|
5504
|
+
const vueSlotted = (pad) => block ? [] : slottedChildMarkup(components, pad, { wired: false, ids: false });
|
|
4043
5505
|
const standaloneCompanionTags = components.filter(
|
|
4044
|
-
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !
|
|
5506
|
+
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !ARTIFACT_SPLIT_TAGS.has(t) && !(block && BLOCK_TAGS.has(t)) && !slottedTags.has(t) && !(attachments && ATTACHMENT_TAGS.has(t))
|
|
4045
5507
|
);
|
|
4046
5508
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
4047
5509
|
const companionLines = [];
|
|
@@ -4096,19 +5558,41 @@ function renderVue(components, ctx) {
|
|
|
4096
5558
|
fromSubmitEvent: true
|
|
4097
5559
|
})
|
|
4098
5560
|
} : {};
|
|
5561
|
+
const blockRead = `(threads.value.find((t) => t.id === targetId)?.messages ?? [])`;
|
|
5562
|
+
const blockPrelude = block ? [
|
|
5563
|
+
...BLOCK_TARGET_ID_NOTE.map((l) => `// ${l}`),
|
|
5564
|
+
`const targetId = activeId.value ?? crypto.randomUUID();`,
|
|
5565
|
+
`// The record is born on its first turn — with YOUR title policy.`,
|
|
5566
|
+
`setThreads((prev) => prev.some((t) => t.id === targetId)`,
|
|
5567
|
+
` ? prev`,
|
|
5568
|
+
` : [{ id: targetId, title: deriveTitle(value), updatedAt: new Date().toISOString(), messages: [] }, ...prev]);`,
|
|
5569
|
+
`activeId.value = targetId;`,
|
|
5570
|
+
...BLOCK_BIND_NOTE.map((l) => `// ${l}`),
|
|
5571
|
+
`const setMessages = bindThreadMessages<Thread>(setThreads, targetId, {`,
|
|
5572
|
+
` touch: (t) => ({ ...t, updatedAt: new Date().toISOString() }),`,
|
|
5573
|
+
`});`,
|
|
5574
|
+
`const controller = sessions.begin(targetId);`
|
|
5575
|
+
] : [];
|
|
4099
5576
|
const onSubmitBody = realStreamBody({
|
|
4100
5577
|
pad: " ",
|
|
4101
|
-
read: "messages.value",
|
|
4102
|
-
commitSet: (expr) => `messages.value = ${expr};`,
|
|
4103
|
-
setterAdapter: "(fn) => { messages.value = fn(messages.value); }",
|
|
4104
|
-
setLoading: (v2) => `loading.value = ${v2};`,
|
|
5578
|
+
read: block ? blockRead : "messages.value",
|
|
5579
|
+
commitSet: block ? (expr) => `setMessages(() => ${expr});` : (expr) => `messages.value = ${expr};`,
|
|
5580
|
+
setterAdapter: block ? "setMessages" : "(fn) => { messages.value = fn(messages.value); }",
|
|
5581
|
+
setLoading: block ? (v2) => v2 === "true" ? `// loading for THIS thread now derives from the sessions map (streamingIds).` : `sessions.end(targetId, controller);` : (v2) => `loading.value = ${v2};`,
|
|
4105
5582
|
bodyPayload: realBodyPayload({ defaultModel, tools: emitTools }),
|
|
4106
5583
|
strictRoles: true,
|
|
4107
5584
|
toolLoop: emitToolLoop,
|
|
4108
5585
|
cards: ctx.emitCards,
|
|
4109
|
-
thread: liveThreadBinding("messages.value", "(fn) => { messages.value = fn(messages.value); }"),
|
|
5586
|
+
thread: block ? blockLiveThread(blockRead) : liveThreadBinding("messages.value", "(fn) => { messages.value = fn(messages.value); }"),
|
|
4110
5587
|
mock: isMock,
|
|
4111
|
-
...
|
|
5588
|
+
...block ? { fetchSignal: "controller.signal" } : {},
|
|
5589
|
+
...attachmentSubmitOpts,
|
|
5590
|
+
...block ? {
|
|
5591
|
+
afterValue: [
|
|
5592
|
+
...attachmentSubmitOpts.afterValue ?? [],
|
|
5593
|
+
...blockPrelude
|
|
5594
|
+
]
|
|
5595
|
+
} : {}
|
|
4112
5596
|
});
|
|
4113
5597
|
const mockInit = isMock ? mockResponderInit() : [];
|
|
4114
5598
|
const modelInit = defaultModel ? [
|
|
@@ -4119,7 +5603,39 @@ function renderVue(components, ctx) {
|
|
|
4119
5603
|
const cardPropAssign = ctx.emitCards ? [`cardTypes: cards.tags`, `cardSchemas: cards.validationSchemas`] : [];
|
|
4120
5604
|
const toolsLines = emitTools ? toolSchemaLines("", ctx.cardProvider) : [];
|
|
4121
5605
|
const runnerLines = emitToolLoop ? toolRunnerLines("") : [];
|
|
4122
|
-
const sampleSeed = [
|
|
5606
|
+
const sampleSeed = block ? [
|
|
5607
|
+
`// ── The block's state: your records + the kit's session/persistence helpers ──`,
|
|
5608
|
+
`const threads = ref<Thread[]>(loadThreads());`,
|
|
5609
|
+
`// null means a DRAFT: "New chat" was pressed and nothing is stored yet. A`,
|
|
5610
|
+
`// record is created on its first turn, so the rail never fills with empties.`,
|
|
5611
|
+
`const activeId = ref<string | null>(threads.value[0]?.id ?? null);`,
|
|
5612
|
+
`// One id per in-flight reply — a stream keeps running when you switch away.`,
|
|
5613
|
+
`const streamingIds = ref<string[]>([]);`,
|
|
5614
|
+
...BLOCK_SESSIONS_NOTE.map((l) => `// ${l}`),
|
|
5615
|
+
`const sessions = createThreadSessions((ids) => { streamingIds.value = ids; });`,
|
|
5616
|
+
`// The save scheduler: the debounce/flush MECHANICS are the kit's; the delay,`,
|
|
5617
|
+
`// the save target and what a failed save means are YOURS (see saveThreads).`,
|
|
5618
|
+
`const saver = createSaveScheduler(saveThreads, { delayMs: 250 });`,
|
|
5619
|
+
`watch(threads, () => saver.schedule(threads.value));`,
|
|
5620
|
+
`// Every write goes through this — a NEW array per write is what notifies.`,
|
|
5621
|
+
`const setThreads = (updater: (prev: Thread[]) => Thread[]): void => {`,
|
|
5622
|
+
` threads.value = updater(threads.value);`,
|
|
5623
|
+
`};`,
|
|
5624
|
+
`const active = computed(() => threads.value.find((t) => t.id === activeId.value) ?? null);`,
|
|
5625
|
+
`const messages = computed(() => active.value?.messages ?? []);`,
|
|
5626
|
+
`const loading = computed(() => { const id = activeId.value; return id !== null && streamingIds.value.includes(id); });`,
|
|
5627
|
+
`// The rail's row shape is a VIEW of your record — titles and counts, no`,
|
|
5628
|
+
`// message bodies. The rail renders the array in the order it is given.`,
|
|
5629
|
+
`const rows = computed(() => threads.value.map((t) => ({ id: t.id, title: t.title, messageCount: t.messages.length, updatedAt: t.updatedAt })));`,
|
|
5630
|
+
``,
|
|
5631
|
+
`function onSelect(event: Event) {`,
|
|
5632
|
+
` activeId.value = (event as CustomEvent<{ id: string }>).detail.id;`,
|
|
5633
|
+
`}`,
|
|
5634
|
+
`function onNewChat() {`,
|
|
5635
|
+
` // A DRAFT, not a record: it becomes a thread on its first turn.`,
|
|
5636
|
+
` activeId.value = null;`,
|
|
5637
|
+
`}`
|
|
5638
|
+
] : [
|
|
4123
5639
|
...hasEmbedded ? sampleSeedComment(isMock, "", (literal) => [
|
|
4124
5640
|
`const messages = ref<ChatMessage[]>([${literal}]);`
|
|
4125
5641
|
]) : [],
|
|
@@ -4136,7 +5652,7 @@ function renderVue(components, ctx) {
|
|
|
4136
5652
|
` if (el) el.sources = sampleSources;`,
|
|
4137
5653
|
`});`
|
|
4138
5654
|
] : [];
|
|
4139
|
-
const vueImports = `import { ref, onMounted } from 'vue';`;
|
|
5655
|
+
const vueImports = block ? `import { computed, onMounted, ref, watch } from 'vue';` : `import { ref, onMounted } from 'vue';`;
|
|
4140
5656
|
const workspaceTemplate = workspace ? [
|
|
4141
5657
|
` <!-- SCAF-14: workspace split — chat pane left, artifact preview right. -->`,
|
|
4142
5658
|
` <!-- kai-resizable needs kai-resizable-item children to render panels. -->`,
|
|
@@ -4149,7 +5665,9 @@ function renderVue(components, ctx) {
|
|
|
4149
5665
|
` suggestion-mode="submit"`,
|
|
4150
5666
|
` style="${p.chatFill}"`,
|
|
4151
5667
|
` @kai-submit="onSubmit"`,
|
|
4152
|
-
`
|
|
5668
|
+
` >`,
|
|
5669
|
+
...vueSlotted(" "),
|
|
5670
|
+
` </kai-chat>`,
|
|
4153
5671
|
` </kai-resizable-item>`,
|
|
4154
5672
|
` <kai-resizable-item min="280px">`,
|
|
4155
5673
|
` <!-- Replace src with your artifact URL or set .files for multi-file preview. -->`,
|
|
@@ -4166,7 +5684,9 @@ function renderVue(components, ctx) {
|
|
|
4166
5684
|
` suggestion-mode="submit"`,
|
|
4167
5685
|
` style="${p.chatFill}"`,
|
|
4168
5686
|
` @kai-submit="onSubmit"`,
|
|
4169
|
-
`
|
|
5687
|
+
` >`,
|
|
5688
|
+
...vueSlotted(" "),
|
|
5689
|
+
` </kai-chat>`,
|
|
4170
5690
|
companions
|
|
4171
5691
|
];
|
|
4172
5692
|
return [
|
|
@@ -4184,16 +5704,18 @@ function renderVue(components, ctx) {
|
|
|
4184
5704
|
cards: ctx.emitCards,
|
|
4185
5705
|
cardTools: ctx.cardProvider !== null,
|
|
4186
5706
|
mock: isMock,
|
|
4187
|
-
attachments
|
|
5707
|
+
attachments,
|
|
5708
|
+
workspaceBlock: block
|
|
4188
5709
|
}),
|
|
4189
5710
|
`import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
4190
5711
|
vueImports,
|
|
4191
5712
|
``,
|
|
4192
5713
|
...mockInit,
|
|
4193
5714
|
``,
|
|
5715
|
+
...block ? [blockThreadStoreLines("").join("\n"), ``] : [],
|
|
4194
5716
|
...cardsInit,
|
|
4195
5717
|
...sampleSeed,
|
|
4196
|
-
`const loading = ref(false)
|
|
5718
|
+
...block ? [] : [`const loading = ref(false);`],
|
|
4197
5719
|
`const suggestions = ${jsArray(suggestions)};`,
|
|
4198
5720
|
...modelInit,
|
|
4199
5721
|
...toolsLines,
|
|
@@ -4209,6 +5731,14 @@ function renderVue(components, ctx) {
|
|
|
4209
5731
|
...ctx.emitCards ? CARD_PROP_COMMENT.map((l) => ` ${l}`) : [],
|
|
4210
5732
|
` const el = document.querySelector('kai-chat');`,
|
|
4211
5733
|
` if (el) Object.assign(el, { messages: messages.value, loading: loading.value, suggestions${cardPropAssign.length ? `, ${cardPropAssign.join(", ")}` : ""} });`,
|
|
5734
|
+
...block ? [
|
|
5735
|
+
` // The rail's array props follow the same upgrade rule.`,
|
|
5736
|
+
` await customElements.whenDefined('kai-conversations');`,
|
|
5737
|
+
` const rail = document.querySelector('kai-conversations');`,
|
|
5738
|
+
` if (rail) Object.assign(rail, { conversations: rows.value, activeId: activeId.value ?? undefined });`,
|
|
5739
|
+
` // Flush the pending write when the tab closes mid-debounce (or mid-stream).`,
|
|
5740
|
+
` window.addEventListener('beforeunload', saver.flush);`
|
|
5741
|
+
] : [],
|
|
4212
5742
|
`});`,
|
|
4213
5743
|
``,
|
|
4214
5744
|
`async function onSubmit(e: ${attachments ? "CustomEvent<{ value: string; attachments: AttachmentData[] }>" : "CustomEvent<{ value: string }>"}) {`,
|
|
@@ -4218,18 +5748,49 @@ function renderVue(components, ctx) {
|
|
|
4218
5748
|
``,
|
|
4219
5749
|
`<template>`,
|
|
4220
5750
|
` <div style="${p.style}">`,
|
|
4221
|
-
...
|
|
4222
|
-
|
|
5751
|
+
...block ? [
|
|
5752
|
+
` <!-- ── The workspace BLOCK: the shell + the rail + your thread records ── -->`,
|
|
5753
|
+
...htmlComment(
|
|
5754
|
+
[
|
|
5755
|
+
`<kai-workspace> is the chat-agnostic layout shell: header/start/main/`,
|
|
5756
|
+
`end/footer slots, aside resize + collapse, and a mobile drawer below`,
|
|
5757
|
+
`the drawer-below width. It knows nothing about chat.`
|
|
5758
|
+
],
|
|
5759
|
+
" "
|
|
5760
|
+
),
|
|
5761
|
+
` <kai-workspace drawer-below="720" style="display:block;width:100%;height:100%">`,
|
|
5762
|
+
...htmlComment(BLOCK_RAIL_NOTE, " "),
|
|
5763
|
+
` <!-- New chat = a DRAFT (activeId null): the record appears on the first turn. -->`,
|
|
5764
|
+
` <kai-conversations`,
|
|
5765
|
+
` slot="start"`,
|
|
5766
|
+
` :conversations.prop="rows"`,
|
|
5767
|
+
` :activeId.prop="activeId ?? undefined"`,
|
|
5768
|
+
` style="display:block;height:100%"`,
|
|
5769
|
+
` @kai-conversation-select="onSelect"`,
|
|
5770
|
+
` @kai-new-chat="onNewChat"`,
|
|
5771
|
+
` />`,
|
|
5772
|
+
` <!-- Unnamed children project into the shell's MAIN region. -->`,
|
|
5773
|
+
` <div style="display:flex;flex-direction:column;height:100%;min-height:0">`,
|
|
5774
|
+
...indentEmitted(
|
|
5775
|
+
[...attachmentTemplate ? [attachmentTemplate] : [], ...workspaceTemplate],
|
|
5776
|
+
" "
|
|
5777
|
+
),
|
|
5778
|
+
` </div>`,
|
|
5779
|
+
` </kai-workspace>`
|
|
5780
|
+
] : [...attachmentTemplate ? [attachmentTemplate] : [], ...workspaceTemplate],
|
|
4223
5781
|
` </div>`,
|
|
4224
5782
|
`</template>`
|
|
4225
5783
|
].filter((l) => l !== "").join("\n");
|
|
4226
5784
|
}
|
|
4227
5785
|
function renderSvelte(components, ctx) {
|
|
4228
5786
|
const { p, emptyHint, suggestions, isMock, defaultModel, emitTools, emitToolLoop } = ctx;
|
|
4229
|
-
const workspace =
|
|
5787
|
+
const workspace = isArtifactSplit(components);
|
|
5788
|
+
const block = isWorkspaceBlock(components);
|
|
4230
5789
|
const attachments = hasAttachments(components);
|
|
5790
|
+
const slottedTags = new Set(slottedInChat(components).map((s) => s.tag));
|
|
5791
|
+
const svelteSlotted = (pad) => block ? [] : slottedChildMarkup(components, pad, { wired: false, ids: false });
|
|
4231
5792
|
const standaloneCompanionTags = components.filter(
|
|
4232
|
-
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !
|
|
5793
|
+
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !ARTIFACT_SPLIT_TAGS.has(t) && !(block && BLOCK_TAGS.has(t)) && !slottedTags.has(t) && !(attachments && ATTACHMENT_TAGS.has(t))
|
|
4233
5794
|
);
|
|
4234
5795
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
4235
5796
|
const hasSourcesCompanion = standaloneCompanionTags.includes("kai-sources");
|
|
@@ -4257,19 +5818,41 @@ function renderSvelte(components, ctx) {
|
|
|
4257
5818
|
fromSubmitEvent: true
|
|
4258
5819
|
})
|
|
4259
5820
|
} : {};
|
|
5821
|
+
const blockRead = `(threads.find((t) => t.id === targetId)?.messages ?? [])`;
|
|
5822
|
+
const blockPrelude = block ? [
|
|
5823
|
+
...BLOCK_TARGET_ID_NOTE.map((l) => `// ${l}`),
|
|
5824
|
+
`const targetId = activeId ?? crypto.randomUUID();`,
|
|
5825
|
+
`// The record is born on its first turn — with YOUR title policy.`,
|
|
5826
|
+
`setThreads((prev) => prev.some((t) => t.id === targetId)`,
|
|
5827
|
+
` ? prev`,
|
|
5828
|
+
` : [{ id: targetId, title: deriveTitle(value), updatedAt: new Date().toISOString(), messages: [] }, ...prev]);`,
|
|
5829
|
+
`activeId = targetId;`,
|
|
5830
|
+
...BLOCK_BIND_NOTE.map((l) => `// ${l}`),
|
|
5831
|
+
`const setMessages = bindThreadMessages<Thread>(setThreads, targetId, {`,
|
|
5832
|
+
` touch: (t) => ({ ...t, updatedAt: new Date().toISOString() }),`,
|
|
5833
|
+
`});`,
|
|
5834
|
+
`const controller = sessions.begin(targetId);`
|
|
5835
|
+
] : [];
|
|
4260
5836
|
const onSubmitBody = realStreamBody({
|
|
4261
5837
|
pad: " ",
|
|
4262
|
-
read: "messages",
|
|
4263
|
-
commitSet: (expr) => `messages = ${expr};`,
|
|
4264
|
-
setterAdapter: "(fn) => { messages = fn(messages); }",
|
|
4265
|
-
setLoading: (v2) => `loading = ${v2};`,
|
|
5838
|
+
read: block ? blockRead : "messages",
|
|
5839
|
+
commitSet: block ? (expr) => `setMessages(() => ${expr});` : (expr) => `messages = ${expr};`,
|
|
5840
|
+
setterAdapter: block ? "setMessages" : "(fn) => { messages = fn(messages); }",
|
|
5841
|
+
setLoading: block ? (v2) => v2 === "true" ? `// loading for THIS thread now derives from the sessions map (streamingIds).` : `sessions.end(targetId, controller);` : (v2) => `loading = ${v2};`,
|
|
4266
5842
|
bodyPayload: realBodyPayload({ defaultModel, tools: emitTools }),
|
|
4267
5843
|
strictRoles: true,
|
|
4268
5844
|
toolLoop: emitToolLoop,
|
|
4269
5845
|
cards: ctx.emitCards,
|
|
4270
|
-
thread: liveThreadBinding("messages", "(fn) => { messages = fn(messages); }"),
|
|
5846
|
+
thread: block ? blockLiveThread(blockRead) : liveThreadBinding("messages", "(fn) => { messages = fn(messages); }"),
|
|
4271
5847
|
mock: isMock,
|
|
4272
|
-
...
|
|
5848
|
+
...block ? { fetchSignal: "controller.signal" } : {},
|
|
5849
|
+
...attachmentSubmitOpts,
|
|
5850
|
+
...block ? {
|
|
5851
|
+
afterValue: [
|
|
5852
|
+
...attachmentSubmitOpts.afterValue ?? [],
|
|
5853
|
+
...blockPrelude
|
|
5854
|
+
]
|
|
5855
|
+
} : {}
|
|
4273
5856
|
});
|
|
4274
5857
|
const mockInit = isMock ? mockResponderInit(" ") : [];
|
|
4275
5858
|
const modelInit = defaultModel ? [
|
|
@@ -4280,7 +5863,46 @@ function renderSvelte(components, ctx) {
|
|
|
4280
5863
|
const cardPropEffect = ctx.emitCards ? ` chatEl.cardTypes = cards.tags; chatEl.cardSchemas = cards.validationSchemas;` : "";
|
|
4281
5864
|
const toolsLines = emitTools ? toolSchemaLines(" ", ctx.cardProvider) : [];
|
|
4282
5865
|
const runnerLines = emitToolLoop ? toolRunnerLines(" ") : [];
|
|
4283
|
-
const sampleMessagesInit = [
|
|
5866
|
+
const sampleMessagesInit = block ? [
|
|
5867
|
+
` // ── The block's state: your records + the kit's session/persistence helpers ─`,
|
|
5868
|
+
` // $state.raw for the same reason messages was: the kit's contract is a NEW`,
|
|
5869
|
+
` // array reference per write, which is exactly what raw state tracks.`,
|
|
5870
|
+
` let threads = $state.raw<Thread[]>(loadThreads());`,
|
|
5871
|
+
` // null means a DRAFT: "New chat" was pressed and nothing is stored yet. A`,
|
|
5872
|
+
` // record is created on its first turn, so the rail never fills with empties.`,
|
|
5873
|
+
` let activeId = $state<string | null>(threads[0]?.id ?? null);`,
|
|
5874
|
+
` // One id per in-flight reply — a stream keeps running when you switch away.`,
|
|
5875
|
+
` let streamingIds = $state.raw<string[]>([]);`,
|
|
5876
|
+
...BLOCK_SESSIONS_NOTE.map((l) => ` // ${l}`),
|
|
5877
|
+
` const sessions = createThreadSessions((ids) => { streamingIds = ids; });`,
|
|
5878
|
+
` // The save scheduler: the debounce/flush MECHANICS are the kit's; the delay,`,
|
|
5879
|
+
` // the save target and what a failed save means are YOURS (see saveThreads).`,
|
|
5880
|
+
` const saver = createSaveScheduler(saveThreads, { delayMs: 250 });`,
|
|
5881
|
+
` $effect(() => { saver.schedule(threads); });`,
|
|
5882
|
+
` // Every write goes through this — a NEW array per write is what notifies.`,
|
|
5883
|
+
` const setThreads = (updater: (prev: Thread[]) => Thread[]): void => {`,
|
|
5884
|
+
` threads = updater(threads);`,
|
|
5885
|
+
` };`,
|
|
5886
|
+
` const active = $derived(threads.find((t) => t.id === activeId) ?? null);`,
|
|
5887
|
+
` const messages = $derived(active?.messages ?? []);`,
|
|
5888
|
+
` const loading = $derived.by(() => { const id = activeId; return id !== null && streamingIds.includes(id); });`,
|
|
5889
|
+
` // The rail's row shape is a VIEW of your record — titles and counts, no`,
|
|
5890
|
+
` // message bodies. The rail renders the array in the order it is given.`,
|
|
5891
|
+
` const rows = $derived(threads.map((t) => ({ id: t.id, title: t.title, messageCount: t.messages.length, updatedAt: t.updatedAt })));`,
|
|
5892
|
+
` // \`bind:this\` writes to this binding, so under runes it must be $state.`,
|
|
5893
|
+
` let railEl = $state<KaiConversationsElement | undefined>(undefined);`,
|
|
5894
|
+
` $effect(() => {`,
|
|
5895
|
+
` if (railEl && defined) { railEl.conversations = rows; railEl.activeId = activeId ?? undefined; }`,
|
|
5896
|
+
` });`,
|
|
5897
|
+
``,
|
|
5898
|
+
` function onSelect(e: CustomEvent<{ id: string }>) {`,
|
|
5899
|
+
` activeId = e.detail.id;`,
|
|
5900
|
+
` }`,
|
|
5901
|
+
` function onNewChat() {`,
|
|
5902
|
+
` // A DRAFT, not a record: it becomes a thread on its first turn.`,
|
|
5903
|
+
` activeId = null;`,
|
|
5904
|
+
` }`
|
|
5905
|
+
] : [
|
|
4284
5906
|
...hasEmbedded ? sampleSeedComment(isMock, " ", (literal) => [
|
|
4285
5907
|
`let messages = $state.raw<ChatMessage[]>([${literal}]);`
|
|
4286
5908
|
]) : [],
|
|
@@ -4327,7 +5949,9 @@ function renderSvelte(components, ctx) {
|
|
|
4327
5949
|
` <!-- kai-resizable needs kai-resizable-item children to render panels. -->`,
|
|
4328
5950
|
` <kai-resizable orientation="horizontal" style="display:block;width:100%;height:100%">`,
|
|
4329
5951
|
` <kai-resizable-item size="40%" min="240px">`,
|
|
4330
|
-
` <kai-chat bind:this={chatEl} suggestion-mode="submit" style="${p.chatFill}" onkai-submit={onSubmit}
|
|
5952
|
+
` <kai-chat bind:this={chatEl} suggestion-mode="submit" style="${p.chatFill}" onkai-submit={onSubmit}>`,
|
|
5953
|
+
...svelteSlotted(" "),
|
|
5954
|
+
` </kai-chat>`,
|
|
4331
5955
|
` </kai-resizable-item>`,
|
|
4332
5956
|
` <kai-resizable-item min="280px">`,
|
|
4333
5957
|
` <!-- Replace src with your artifact URL or set .files for multi-file preview. -->`,
|
|
@@ -4337,7 +5961,9 @@ function renderSvelte(components, ctx) {
|
|
|
4337
5961
|
// Siblings of the SPLIT — see the same line in renderJsx.
|
|
4338
5962
|
companionLines
|
|
4339
5963
|
] : [
|
|
4340
|
-
` <kai-chat bind:this={chatEl} suggestion-mode="submit" style="${p.chatFill}" onkai-submit={onSubmit}
|
|
5964
|
+
` <kai-chat bind:this={chatEl} suggestion-mode="submit" style="${p.chatFill}" onkai-submit={onSubmit}>`,
|
|
5965
|
+
...svelteSlotted(" "),
|
|
5966
|
+
` </kai-chat>`,
|
|
4341
5967
|
companionLines
|
|
4342
5968
|
];
|
|
4343
5969
|
return [
|
|
@@ -4354,6 +5980,7 @@ function renderSvelte(components, ctx) {
|
|
|
4354
5980
|
// on every archetype without kai-sources.
|
|
4355
5981
|
` import type { ${[
|
|
4356
5982
|
"KaiChatElement",
|
|
5983
|
+
...block ? ["KaiConversationsElement"] : [],
|
|
4357
5984
|
...hasSourcesCompanion ? ["KaiSourcesElement"] : [],
|
|
4358
5985
|
...attachments ? ["KaiAttachmentsElement"] : []
|
|
4359
5986
|
].join(", ")} } from '@kitn.ai/ui/elements';`,
|
|
@@ -4364,21 +5991,36 @@ function renderSvelte(components, ctx) {
|
|
|
4364
5991
|
cards: ctx.emitCards,
|
|
4365
5992
|
cardTools: ctx.cardProvider !== null,
|
|
4366
5993
|
mock: isMock,
|
|
4367
|
-
attachments
|
|
5994
|
+
attachments,
|
|
5995
|
+
workspaceBlock: block
|
|
4368
5996
|
}),
|
|
4369
5997
|
` import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
4370
5998
|
` import { onMount } from 'svelte';`,
|
|
4371
5999
|
...mockInit,
|
|
6000
|
+
...block ? [blockThreadStoreLines(" ").join("\n"), ``] : [],
|
|
4372
6001
|
` // \`bind:this\` writes to this binding, so under runes it must be $state.`,
|
|
4373
6002
|
` let chatEl = $state<KaiChatElement | undefined>(undefined);`,
|
|
4374
6003
|
` // SCAF-15: kai-* register via an async dynamic import (SSR-safety). Gate the`,
|
|
4375
6004
|
` // property $effect on the upgrade so the first application isn't dropped`,
|
|
4376
6005
|
` // (props set on a not-yet-upgraded element are lost on upgrade).`,
|
|
4377
6006
|
` let defined = $state(false);`,
|
|
4378
|
-
|
|
6007
|
+
...block ? [
|
|
6008
|
+
` onMount(() => {`,
|
|
6009
|
+
` void Promise.all([`,
|
|
6010
|
+
` customElements.whenDefined('kai-chat'),`,
|
|
6011
|
+
` customElements.whenDefined('kai-conversations'),`,
|
|
6012
|
+
` ]).then(() => { defined = true; });`,
|
|
6013
|
+
` // Flush the pending write when the tab closes mid-debounce (or mid-stream).`,
|
|
6014
|
+
` const flush = () => saver.flush();`,
|
|
6015
|
+
` window.addEventListener('beforeunload', flush);`,
|
|
6016
|
+
` return () => window.removeEventListener('beforeunload', flush);`,
|
|
6017
|
+
` });`
|
|
6018
|
+
] : [
|
|
6019
|
+
` onMount(async () => { await customElements.whenDefined('kai-chat'); defined = true; });`
|
|
6020
|
+
],
|
|
4379
6021
|
...sourcesEl,
|
|
4380
6022
|
...sampleMessagesInit,
|
|
4381
|
-
` let loading = $state(false)
|
|
6023
|
+
...block ? [] : [` let loading = $state(false);`],
|
|
4382
6024
|
` const suggestions: string[] = ${jsArray(suggestions)};`,
|
|
4383
6025
|
...modelInit,
|
|
4384
6026
|
...cardsInit,
|
|
@@ -4398,22 +6040,49 @@ function renderSvelte(components, ctx) {
|
|
|
4398
6040
|
`<\/script>`,
|
|
4399
6041
|
``,
|
|
4400
6042
|
`<div style="${p.style}">`,
|
|
4401
|
-
...
|
|
4402
|
-
|
|
6043
|
+
...block ? [
|
|
6044
|
+
` <!-- ── The workspace BLOCK: the shell + the rail + your thread records ── -->`,
|
|
6045
|
+
...htmlComment(
|
|
6046
|
+
[
|
|
6047
|
+
`<kai-workspace> is the chat-agnostic layout shell: header/start/main/`,
|
|
6048
|
+
`end/footer slots, aside resize + collapse, and a mobile drawer below`,
|
|
6049
|
+
`the drawer-below width. It knows nothing about chat.`
|
|
6050
|
+
],
|
|
6051
|
+
" "
|
|
6052
|
+
),
|
|
6053
|
+
` <kai-workspace drawer-below="720" style="display:block;width:100%;height:100%">`,
|
|
6054
|
+
...htmlComment(BLOCK_RAIL_NOTE, " "),
|
|
6055
|
+
` <!-- New chat = a DRAFT (activeId null): the record appears on the first turn. -->`,
|
|
6056
|
+
` <kai-conversations`,
|
|
6057
|
+
` bind:this={railEl}`,
|
|
6058
|
+
` slot="start"`,
|
|
6059
|
+
` style="display:block;height:100%"`,
|
|
6060
|
+
` onkai-conversation-select={onSelect}`,
|
|
6061
|
+
` onkai-new-chat={onNewChat}`,
|
|
6062
|
+
` ></kai-conversations>`,
|
|
6063
|
+
` <!-- Unnamed children project into the shell's MAIN region. -->`,
|
|
6064
|
+
` <div style="display:flex;flex-direction:column;height:100%;min-height:0">`,
|
|
6065
|
+
...indentEmitted([...attachmentMarkup, ...workspaceMarkup], " "),
|
|
6066
|
+
` </div>`,
|
|
6067
|
+
` </kai-workspace>`
|
|
6068
|
+
] : [...attachmentMarkup, ...workspaceMarkup],
|
|
4403
6069
|
`</div>`
|
|
4404
6070
|
].filter((l, i, arr) => !(l === "" && arr[i - 1] === "" && i === arr.length - 1)).join("\n");
|
|
4405
6071
|
}
|
|
4406
6072
|
function renderTanstackStart(components, ctx) {
|
|
4407
6073
|
const { p, emptyHint, suggestions, isMock, defaultModel, emitTools, emitToolLoop } = ctx;
|
|
4408
6074
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
4409
|
-
const workspace =
|
|
4410
|
-
const
|
|
6075
|
+
const workspace = isArtifactSplit(components);
|
|
6076
|
+
const block = isWorkspaceBlock(components);
|
|
6077
|
+
const renderableTags = [
|
|
6078
|
+
.../* @__PURE__ */ new Set([ALWAYS_EMITTED_TAG, ...components.filter((t) => !MESSAGE_EMBEDDED_TAGS.has(t))])
|
|
6079
|
+
];
|
|
4411
6080
|
const importTags = workspace ? [.../* @__PURE__ */ new Set([...renderableTags.filter((t) => t !== "kai-resizable"), "kai-resizable", "kai-resizable-item"])] : renderableTags;
|
|
4412
6081
|
const wrapperNames = importTags.map(toPascalCase);
|
|
4413
6082
|
const importList = wrapperNames.join(", ");
|
|
4414
6083
|
const attachments = hasAttachments(components);
|
|
4415
6084
|
const standaloneCompanionTags = components.filter(
|
|
4416
|
-
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !
|
|
6085
|
+
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !ARTIFACT_SPLIT_TAGS.has(t) && !(block && BLOCK_TAGS.has(t)) && !(attachments && ATTACHMENT_TAGS.has(t))
|
|
4417
6086
|
);
|
|
4418
6087
|
const companionJsxLines = [];
|
|
4419
6088
|
if (hasEmbedded) {
|
|
@@ -4428,6 +6097,8 @@ function renderTanstackStart(components, ctx) {
|
|
|
4428
6097
|
` {/* Replace sampleSources with your real data. */}`,
|
|
4429
6098
|
` <Sources sources={sampleSources} />`
|
|
4430
6099
|
);
|
|
6100
|
+
} else if (slotPlacementFor(t)) {
|
|
6101
|
+
companionJsxLines.push(...railSiblingNote(t, slotPlacementFor(t)), ` <${toPascalCase(t)} />`);
|
|
4431
6102
|
} else {
|
|
4432
6103
|
companionJsxLines.push(` {/* wire data props — see the component_reference MCP tool */}`);
|
|
4433
6104
|
companionJsxLines.push(` <${toPascalCase(t)} />`);
|
|
@@ -4470,13 +6141,52 @@ function renderTanstackStart(components, ctx) {
|
|
|
4470
6141
|
} : {};
|
|
4471
6142
|
const submitParamType = attachments ? "CustomEvent<{ value: string; attachments: AttachmentData[] }>" : "CustomEvent<{ value: string }>";
|
|
4472
6143
|
const mockInit = isMock ? mockResponderInit() : [];
|
|
4473
|
-
const sampleMessagesInit = [
|
|
6144
|
+
const sampleMessagesInit = block ? [
|
|
6145
|
+
` // ── The block's state: your records + the kit's session/persistence helpers ─`,
|
|
6146
|
+
` const [threads, setThreads] = useState<Thread[]>(loadThreads);`,
|
|
6147
|
+
` // null means a DRAFT: "New chat" was pressed and nothing is stored yet. A`,
|
|
6148
|
+
` // record is created on its first turn, so the rail never fills with empties.`,
|
|
6149
|
+
` const [activeId, setActiveId] = useState<string | null>(threads[0]?.id ?? null);`,
|
|
6150
|
+
` // One id per in-flight reply — a stream keeps running when you switch away,`,
|
|
6151
|
+
` // so this cannot be a single boolean.`,
|
|
6152
|
+
` const [streamingIds, setStreamingIds] = useState<string[]>([]);`,
|
|
6153
|
+
...BLOCK_SESSIONS_NOTE.map((l) => ` // ${l}`),
|
|
6154
|
+
` const [sessions] = useState(() => createThreadSessions(setStreamingIds));`,
|
|
6155
|
+
` // The save scheduler: the debounce/flush MECHANICS are the kit's; the delay,`,
|
|
6156
|
+
` // the save target and what a failed save means are YOURS (see saveThreads).`,
|
|
6157
|
+
` const [saver] = useState(() => createSaveScheduler(saveThreads, { delayMs: 250 }));`,
|
|
6158
|
+
` useEffect(() => { saver.schedule(threads); }, [saver, threads]);`,
|
|
6159
|
+
` useEffect(() => {`,
|
|
6160
|
+
` // Flush the pending write when the tab closes mid-debounce (or mid-stream).`,
|
|
6161
|
+
` const flush = () => saver.flush();`,
|
|
6162
|
+
` window.addEventListener('beforeunload', flush);`,
|
|
6163
|
+
` return () => window.removeEventListener('beforeunload', flush);`,
|
|
6164
|
+
` }, [saver]);`,
|
|
6165
|
+
``,
|
|
6166
|
+
` const active = threads.find((t) => t.id === activeId) ?? null;`,
|
|
6167
|
+
` const messages = active?.messages ?? NO_MESSAGES;`,
|
|
6168
|
+
` const loading = activeId !== null && streamingIds.includes(activeId);`,
|
|
6169
|
+
` // The rail's row shape is a VIEW of your record — titles and counts, no`,
|
|
6170
|
+
` // message bodies. Newest-first is your ordering; the rail renders the array`,
|
|
6171
|
+
` // in the order it is given.`,
|
|
6172
|
+
` const rows = threads.map((t) => ({ id: t.id, title: t.title, messageCount: t.messages.length, updatedAt: t.updatedAt }));`
|
|
6173
|
+
].join("\n") : [
|
|
4474
6174
|
...hasEmbedded ? sampleSeedComment(isMock, " ", (literal) => [
|
|
4475
6175
|
`const sampleMessages: ChatMessage[] = [${literal}];`,
|
|
4476
6176
|
`const [messages, setMessages] = useState<ChatMessage[]>(sampleMessages);`
|
|
4477
6177
|
]) : [],
|
|
4478
6178
|
` const [messages, setMessages] = useState<ChatMessage[]>([]);`
|
|
4479
6179
|
].join("\n");
|
|
6180
|
+
const blockModuleInit = block ? [
|
|
6181
|
+
[
|
|
6182
|
+
`/** A stable reference for the empty thread. \`messages\` is diffed by`,
|
|
6183
|
+
` * reference, so a fresh \`[]\` every render would re-notify for nothing. */`,
|
|
6184
|
+
`const NO_MESSAGES: ChatMessage[] = [];`,
|
|
6185
|
+
``,
|
|
6186
|
+
...blockThreadStoreLines("")
|
|
6187
|
+
].join("\n")
|
|
6188
|
+
] : [];
|
|
6189
|
+
const loadingInit = block ? [] : [` const [loading, setLoading] = useState(false);`];
|
|
4480
6190
|
const sampleSourcesInit = standaloneCompanionTags.includes("kai-sources") ? [
|
|
4481
6191
|
` // Replace sampleSources with your real source data.`,
|
|
4482
6192
|
` const sampleSources = [`,
|
|
@@ -4491,20 +6201,54 @@ function renderTanstackStart(components, ctx) {
|
|
|
4491
6201
|
const cardsInit = ctx.emitCards ? cardRegistryLines("") : [];
|
|
4492
6202
|
const cardProps = (pad) => ctx.emitCards ? [`${pad}cardTypes={cards.tags}`, `${pad}cardSchemas={cards.validationSchemas}`] : [];
|
|
4493
6203
|
const cardPropsNote = (pad) => ctx.emitCards ? jsxComment(CARD_PROP_COMMENT, pad) : [];
|
|
6204
|
+
const blockPrelude = block ? [
|
|
6205
|
+
...BLOCK_TARGET_ID_NOTE.map((l) => `// ${l}`),
|
|
6206
|
+
`const targetId = activeId ?? crypto.randomUUID();`,
|
|
6207
|
+
`// The record is born on its first turn — with YOUR title policy.`,
|
|
6208
|
+
`setThreads((prev) => prev.some((t) => t.id === targetId)`,
|
|
6209
|
+
` ? prev`,
|
|
6210
|
+
` : [{ id: targetId, title: deriveTitle(value), updatedAt: new Date().toISOString(), messages: [] }, ...prev]);`,
|
|
6211
|
+
`setActiveId(targetId);`,
|
|
6212
|
+
...BLOCK_BIND_NOTE.map((l) => `// ${l}`),
|
|
6213
|
+
`const setMessages = bindThreadMessages<Thread>(setThreads, targetId, {`,
|
|
6214
|
+
` touch: (t) => ({ ...t, updatedAt: new Date().toISOString() }),`,
|
|
6215
|
+
`});`,
|
|
6216
|
+
`const controller = sessions.begin(targetId);`
|
|
6217
|
+
] : [];
|
|
6218
|
+
const blockReactThread = {
|
|
6219
|
+
open: ({ pad, userMessage }) => [
|
|
6220
|
+
`${pad}// THE TURN OWNS THE THREAD (same rule as the single-thread scaffold): the`,
|
|
6221
|
+
`${pad}// loop below re-encodes it every round, and React state cannot be read`,
|
|
6222
|
+
`${pad}// back mid-turn. setMessages (the id-bound sink) just projects it.`,
|
|
6223
|
+
`${pad}let thread: ChatMessage[] = [...(threads.find((t) => t.id === targetId)?.messages ?? []), ${userMessage}];`,
|
|
6224
|
+
`${pad}const set: SetMessages = (fn) => { thread = fn(thread); setMessages(() => thread); };`,
|
|
6225
|
+
`${pad}setMessages(() => thread);`
|
|
6226
|
+
],
|
|
6227
|
+
live: "thread",
|
|
6228
|
+
setter: "set"
|
|
6229
|
+
};
|
|
4494
6230
|
const onSubmitBody = realStreamBody({
|
|
4495
6231
|
pad: " ",
|
|
4496
|
-
read: "messages",
|
|
4497
|
-
commitSet: (expr) => `setMessages(${expr});`,
|
|
4498
|
-
// useState's setter IS a SetMessages: both are (updater) => void
|
|
6232
|
+
read: block ? `(threads.find((t) => t.id === targetId)?.messages ?? [])` : "messages",
|
|
6233
|
+
commitSet: block ? (expr) => `setMessages(() => ${expr});` : (expr) => `setMessages(${expr});`,
|
|
6234
|
+
// useState's setter IS a SetMessages: both are (updater) => void — and in
|
|
6235
|
+
// block mode the id-bound sink from bindThreadMessages is one too.
|
|
4499
6236
|
setterAdapter: "setMessages",
|
|
4500
|
-
setLoading: (v2) => `setLoading(${v2});`,
|
|
6237
|
+
setLoading: block ? (v2) => v2 === "true" ? `// loading for THIS thread now derives from the sessions map (streamingIds).` : `sessions.end(targetId, controller);` : (v2) => `setLoading(${v2});`,
|
|
4501
6238
|
bodyPayload: realBodyPayload({ defaultModel, tools: emitTools }),
|
|
4502
6239
|
strictRoles: true,
|
|
4503
6240
|
toolLoop: emitToolLoop,
|
|
4504
6241
|
cards: ctx.emitCards,
|
|
4505
|
-
thread: REACT_THREAD,
|
|
6242
|
+
thread: block ? blockReactThread : REACT_THREAD,
|
|
4506
6243
|
mock: isMock,
|
|
4507
|
-
...
|
|
6244
|
+
...block ? { fetchSignal: "controller.signal" } : {},
|
|
6245
|
+
...attachmentSubmitOpts,
|
|
6246
|
+
...block ? {
|
|
6247
|
+
afterValue: [
|
|
6248
|
+
...attachmentSubmitOpts.afterValue ?? [],
|
|
6249
|
+
...blockPrelude
|
|
6250
|
+
]
|
|
6251
|
+
} : {}
|
|
4508
6252
|
});
|
|
4509
6253
|
const filePathNote = [
|
|
4510
6254
|
`// TanStack Start route file — save as: src/routes/chat.tsx`,
|
|
@@ -4520,11 +6264,65 @@ function renderTanstackStart(components, ctx) {
|
|
|
4520
6264
|
`// server function: src/routes/api/chat.ts with server.handlers.POST — block (2).`,
|
|
4521
6265
|
``
|
|
4522
6266
|
];
|
|
6267
|
+
const chatJsx = (pad) => [
|
|
6268
|
+
...cardPropsNote(pad),
|
|
6269
|
+
`${pad}<Chat`,
|
|
6270
|
+
`${pad} messages={messages}`,
|
|
6271
|
+
`${pad} loading={loading}`,
|
|
6272
|
+
`${pad} suggestions={suggestions}`,
|
|
6273
|
+
`${pad} suggestionMode="submit"`,
|
|
6274
|
+
...cardProps(`${pad} `),
|
|
6275
|
+
`${pad} onSubmit={onSubmit}`,
|
|
6276
|
+
`${pad} style={{ ${jsxStyle(p.chatFill)} }}`,
|
|
6277
|
+
`${pad}/>`
|
|
6278
|
+
];
|
|
6279
|
+
const splitJsx = [
|
|
6280
|
+
` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
|
|
6281
|
+
` {/* Resizable needs ResizableItem children to render panels. */}`,
|
|
6282
|
+
` <Resizable orientation="horizontal" style={{ display: 'block', width: '100%', height: '100%' }}>`,
|
|
6283
|
+
` <ResizableItem size="40%" min="240px">`,
|
|
6284
|
+
...chatJsx(" "),
|
|
6285
|
+
` </ResizableItem>`,
|
|
6286
|
+
` <ResizableItem min="280px">`,
|
|
6287
|
+
` {/* Replace src + files with your real artifact data (files is required: array/object props are never optional attributes on a kai-* element). */}`,
|
|
6288
|
+
` <Artifact src="https://example.com" files={[{ path: 'index.html', url: 'https://example.com' }]} style={{ width: '100%', height: '100%' }} />`,
|
|
6289
|
+
` </ResizableItem>`,
|
|
6290
|
+
` </Resizable>`
|
|
6291
|
+
];
|
|
6292
|
+
const mainJsx = [
|
|
6293
|
+
...attachmentJsx ? [attachmentJsx] : [],
|
|
6294
|
+
...workspace ? [...splitJsx, companions] : [...chatJsx(" "), companions]
|
|
6295
|
+
];
|
|
6296
|
+
const surfaceJsx = block ? [
|
|
6297
|
+
` {/* ── The workspace BLOCK: the shell + the rail + your thread records ── */}`,
|
|
6298
|
+
` {/* <Workspace> is the chat-agnostic layout shell (header/start/main/end/`,
|
|
6299
|
+
` footer slots, aside resize + collapse, a mobile drawer below`,
|
|
6300
|
+
` drawerBelow px). It knows nothing about chat. */}`,
|
|
6301
|
+
` <Workspace drawerBelow={720} style={{ display: 'block', width: '100%', height: '100%' }}>`,
|
|
6302
|
+
...jsxComment(BLOCK_RAIL_NOTE, " "),
|
|
6303
|
+
` {/* The React wrappers forward no slot prop, so a plain div carries the`,
|
|
6304
|
+
` slot attribute into the shell's start aside. */}`,
|
|
6305
|
+
` <div slot="start" style={{ height: '100%' }}>`,
|
|
6306
|
+
` {/* New chat = a DRAFT (activeId null): the record appears on the first turn. */}`,
|
|
6307
|
+
` <Conversations`,
|
|
6308
|
+
` conversations={rows}`,
|
|
6309
|
+
` activeId={activeId ?? undefined}`,
|
|
6310
|
+
` onConversationSelect={(e) => setActiveId(e.detail.id)}`,
|
|
6311
|
+
` onNewChat={() => setActiveId(null)}`,
|
|
6312
|
+
` style={{ display: 'block', height: '100%' }}`,
|
|
6313
|
+
` />`,
|
|
6314
|
+
` </div>`,
|
|
6315
|
+
` {/* Unnamed children project into the shell's MAIN region. */}`,
|
|
6316
|
+
` <div style={{ display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 }}>`,
|
|
6317
|
+
...indentEmitted(mainJsx, " "),
|
|
6318
|
+
` </div>`,
|
|
6319
|
+
` </Workspace>`
|
|
6320
|
+
] : mainJsx;
|
|
4523
6321
|
return [
|
|
4524
6322
|
...filePathNote,
|
|
4525
6323
|
// TanStack Start uses @tanstack/react-router's createFileRoute
|
|
4526
6324
|
`import { createFileRoute } from '@tanstack/react-router'`,
|
|
4527
|
-
`import { useState } from 'react'`,
|
|
6325
|
+
`import { ${block ? "useEffect, useState" : "useState"} } from 'react'`,
|
|
4528
6326
|
// Elements registration: the library is SSR-import-safe; top-level import is safe here
|
|
4529
6327
|
`import '@kitn.ai/ui/elements'; // registers <kai-*> — required, must come first`,
|
|
4530
6328
|
`import { ${importList} } from '@kitn.ai/ui/react'`,
|
|
@@ -4535,7 +6333,8 @@ function renderTanstackStart(components, ctx) {
|
|
|
4535
6333
|
cards: ctx.emitCards,
|
|
4536
6334
|
cardTools: ctx.cardProvider !== null,
|
|
4537
6335
|
mock: isMock,
|
|
4538
|
-
attachments
|
|
6336
|
+
attachments,
|
|
6337
|
+
workspaceBlock: block
|
|
4539
6338
|
}),
|
|
4540
6339
|
`import '@kitn.ai/ui/theme.tokens.css' // compiled token defaults`,
|
|
4541
6340
|
``,
|
|
@@ -4544,6 +6343,7 @@ function renderTanstackStart(components, ctx) {
|
|
|
4544
6343
|
...mockInit,
|
|
4545
6344
|
``,
|
|
4546
6345
|
...attachmentModuleInit,
|
|
6346
|
+
...blockModuleInit,
|
|
4547
6347
|
...cardsInit,
|
|
4548
6348
|
`// ssr: false keeps the Solid-based web component client-only.`,
|
|
4549
6349
|
`// Server HTML for /chat omits <kai-chat> → no hydration mismatch.`,
|
|
@@ -4554,7 +6354,7 @@ function renderTanstackStart(components, ctx) {
|
|
|
4554
6354
|
``,
|
|
4555
6355
|
`function ChatPage() {`,
|
|
4556
6356
|
sampleMessagesInit,
|
|
4557
|
-
|
|
6357
|
+
...loadingInit,
|
|
4558
6358
|
` const suggestions = ${jsArray(suggestions)};`,
|
|
4559
6359
|
...attachmentStateInit ? [attachmentStateInit] : [],
|
|
4560
6360
|
...sampleSourcesInit ? [sampleSourcesInit] : [],
|
|
@@ -4568,43 +6368,7 @@ function renderTanstackStart(components, ctx) {
|
|
|
4568
6368
|
``,
|
|
4569
6369
|
` return (`,
|
|
4570
6370
|
` <main style={{ ${jsxStyle(p.style)} }}>`,
|
|
4571
|
-
...
|
|
4572
|
-
...workspace ? [
|
|
4573
|
-
` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
|
|
4574
|
-
` {/* Resizable needs ResizableItem children to render panels. */}`,
|
|
4575
|
-
` <Resizable orientation="horizontal" style={{ display: 'block', width: '100%', height: '100%' }}>`,
|
|
4576
|
-
` <ResizableItem size="40%" min="240px">`,
|
|
4577
|
-
...cardPropsNote(" "),
|
|
4578
|
-
` <Chat`,
|
|
4579
|
-
` messages={messages}`,
|
|
4580
|
-
` loading={loading}`,
|
|
4581
|
-
` suggestions={suggestions}`,
|
|
4582
|
-
` suggestionMode="submit"`,
|
|
4583
|
-
...cardProps(" "),
|
|
4584
|
-
` onSubmit={onSubmit}`,
|
|
4585
|
-
` style={{ ${jsxStyle(p.chatFill)} }}`,
|
|
4586
|
-
` />`,
|
|
4587
|
-
` </ResizableItem>`,
|
|
4588
|
-
` <ResizableItem min="280px">`,
|
|
4589
|
-
` {/* Replace src + files with your real artifact data (files is required: array/object props are never optional attributes on a kai-* element). */}`,
|
|
4590
|
-
` <Artifact src="https://example.com" files={[{ path: 'index.html', url: 'https://example.com' }]} style={{ width: '100%', height: '100%' }} />`,
|
|
4591
|
-
` </ResizableItem>`,
|
|
4592
|
-
` </Resizable>`,
|
|
4593
|
-
// Siblings of the SPLIT — see the same line in the other JSX branch.
|
|
4594
|
-
companions
|
|
4595
|
-
] : [
|
|
4596
|
-
...cardPropsNote(" "),
|
|
4597
|
-
` <Chat`,
|
|
4598
|
-
` messages={messages}`,
|
|
4599
|
-
` loading={loading}`,
|
|
4600
|
-
` suggestions={suggestions}`,
|
|
4601
|
-
` suggestionMode="submit"`,
|
|
4602
|
-
...cardProps(" "),
|
|
4603
|
-
` onSubmit={onSubmit}`,
|
|
4604
|
-
` style={{ ${jsxStyle(p.chatFill)} }}`,
|
|
4605
|
-
` />`,
|
|
4606
|
-
companions
|
|
4607
|
-
],
|
|
6371
|
+
...surfaceJsx,
|
|
4608
6372
|
` </main>`,
|
|
4609
6373
|
` );`,
|
|
4610
6374
|
`}`
|
|
@@ -4612,10 +6376,12 @@ function renderTanstackStart(components, ctx) {
|
|
|
4612
6376
|
}
|
|
4613
6377
|
function renderAngular(components, ctx) {
|
|
4614
6378
|
const { p, emptyHint, suggestions, isMock, defaultModel, emitTools, emitToolLoop } = ctx;
|
|
4615
|
-
const workspace =
|
|
6379
|
+
const workspace = isArtifactSplit(components);
|
|
6380
|
+
const block = isWorkspaceBlock(components);
|
|
4616
6381
|
const attachments = hasAttachments(components);
|
|
6382
|
+
const slottedTags = new Set(slottedInChat(components).map((s) => s.tag));
|
|
4617
6383
|
const standaloneCompanionTags = components.filter(
|
|
4618
|
-
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !
|
|
6384
|
+
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !ARTIFACT_SPLIT_TAGS.has(t) && !(block && BLOCK_TAGS.has(t)) && !slottedTags.has(t) && !(attachments && ATTACHMENT_TAGS.has(t))
|
|
4619
6385
|
);
|
|
4620
6386
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
4621
6387
|
const hasSourcesCompanion = standaloneCompanionTags.includes("kai-sources");
|
|
@@ -4679,19 +6445,41 @@ function renderAngular(components, ctx) {
|
|
|
4679
6445
|
const read = "this.messages()";
|
|
4680
6446
|
const commit = (value) => `this.messages.set(${value});`;
|
|
4681
6447
|
const setter = "(fn) => this.messages.set(fn(this.messages()))";
|
|
6448
|
+
const blockRead = `(this.threads().find((t) => t.id === targetId)?.messages ?? [])`;
|
|
6449
|
+
const blockPrelude = block ? [
|
|
6450
|
+
...BLOCK_TARGET_ID_NOTE.map((l) => `// ${l}`),
|
|
6451
|
+
`const targetId = this.activeId() ?? crypto.randomUUID();`,
|
|
6452
|
+
`// The record is born on its first turn — with YOUR title policy.`,
|
|
6453
|
+
`this.setThreads((prev) => prev.some((t) => t.id === targetId)`,
|
|
6454
|
+
` ? prev`,
|
|
6455
|
+
` : [{ id: targetId, title: deriveTitle(value), updatedAt: new Date().toISOString(), messages: [] }, ...prev]);`,
|
|
6456
|
+
`this.activeId.set(targetId);`,
|
|
6457
|
+
...BLOCK_BIND_NOTE.map((l) => `// ${l}`),
|
|
6458
|
+
`const setMessages = bindThreadMessages<Thread>(this.setThreads, targetId, {`,
|
|
6459
|
+
` touch: (t) => ({ ...t, updatedAt: new Date().toISOString() }),`,
|
|
6460
|
+
`});`,
|
|
6461
|
+
`const controller = this.sessions.begin(targetId);`
|
|
6462
|
+
] : [];
|
|
4682
6463
|
const onSubmitBody = realStreamBody({
|
|
4683
6464
|
pad: " ",
|
|
4684
|
-
read,
|
|
4685
|
-
commitSet: (expr) => commit(expr),
|
|
4686
|
-
setterAdapter: setter,
|
|
4687
|
-
setLoading: (v2) => `this.loading.set(${v2});`,
|
|
6465
|
+
read: block ? blockRead : read,
|
|
6466
|
+
commitSet: block ? (expr) => `setMessages(() => ${expr});` : (expr) => commit(expr),
|
|
6467
|
+
setterAdapter: block ? "setMessages" : setter,
|
|
6468
|
+
setLoading: block ? (v2) => v2 === "true" ? `// loading for THIS thread now derives from the sessions map (streamingIds).` : `this.sessions.end(targetId, controller);` : (v2) => `this.loading.set(${v2});`,
|
|
4688
6469
|
bodyPayload: realBodyPayload({ defaultModel, tools: emitTools }),
|
|
4689
6470
|
strictRoles: true,
|
|
4690
6471
|
toolLoop: emitToolLoop,
|
|
4691
6472
|
cards: ctx.emitCards,
|
|
4692
|
-
thread: accessorThreadBinding(read, commit, setter),
|
|
6473
|
+
thread: block ? blockLiveThread(blockRead) : accessorThreadBinding(read, commit, setter),
|
|
4693
6474
|
mock: isMock,
|
|
4694
|
-
...
|
|
6475
|
+
...block ? { fetchSignal: "controller.signal" } : {},
|
|
6476
|
+
...attachmentSubmitOpts,
|
|
6477
|
+
...block ? {
|
|
6478
|
+
afterValue: [
|
|
6479
|
+
...attachmentSubmitOpts.afterValue ?? [],
|
|
6480
|
+
...blockPrelude
|
|
6481
|
+
]
|
|
6482
|
+
} : {}
|
|
4695
6483
|
});
|
|
4696
6484
|
const modelInit = defaultModel ? [
|
|
4697
6485
|
`// SCAF-8: change this model id to another id THIS PROVIDER accepts.`,
|
|
@@ -4706,7 +6494,31 @@ function renderAngular(components, ctx) {
|
|
|
4706
6494
|
` cardTypes: cards.tags,`,
|
|
4707
6495
|
` cardSchemas: cards.validationSchemas,`
|
|
4708
6496
|
] : [];
|
|
4709
|
-
const sampleSeed = [
|
|
6497
|
+
const sampleSeed = block ? [
|
|
6498
|
+
` // ── The block's state: your records + the kit's session/persistence helpers ─`,
|
|
6499
|
+
` readonly threads = signal<Thread[]>(loadThreads());`,
|
|
6500
|
+
` // null means a DRAFT: "New chat" was pressed and nothing is stored yet. A`,
|
|
6501
|
+
` // record is created on its first turn, so the rail never fills with empties.`,
|
|
6502
|
+
` readonly activeId = signal<string | null>(this.threads()[0]?.id ?? null);`,
|
|
6503
|
+
` // One id per in-flight reply — a stream keeps running when you switch away.`,
|
|
6504
|
+
` readonly streamingIds = signal<string[]>([]);`,
|
|
6505
|
+
...BLOCK_SESSIONS_NOTE.map((l) => ` // ${l}`),
|
|
6506
|
+
` private readonly sessions = createThreadSessions((ids) => this.streamingIds.set(ids));`,
|
|
6507
|
+
` // The save scheduler: the debounce/flush MECHANICS are the kit's; the delay,`,
|
|
6508
|
+
` // the save target and what a failed save means are YOURS (see saveThreads).`,
|
|
6509
|
+
` private readonly saver = createSaveScheduler(saveThreads, { delayMs: 250 });`,
|
|
6510
|
+
` // Every write goes through this — a NEW array per write is what notifies.`,
|
|
6511
|
+
` private readonly setThreads = (updater: (prev: Thread[]) => Thread[]): void =>`,
|
|
6512
|
+
` this.threads.update(updater);`,
|
|
6513
|
+
` readonly active = computed(() => this.threads().find((t) => t.id === this.activeId()) ?? null);`,
|
|
6514
|
+
` readonly messages = computed(() => this.active()?.messages ?? []);`,
|
|
6515
|
+
` readonly loading = computed(() => { const id = this.activeId(); return id !== null && this.streamingIds().includes(id); });`,
|
|
6516
|
+
` // The rail's row shape is a VIEW of your record — titles and counts, no`,
|
|
6517
|
+
` // message bodies. The rail renders the array in the order it is given.`,
|
|
6518
|
+
` readonly rows = computed(() => this.threads().map((t) => ({ id: t.id, title: t.title, messageCount: t.messages.length, updatedAt: t.updatedAt })));`,
|
|
6519
|
+
` // The template cannot spell \`undefined\` portably, so the ?? lives here.`,
|
|
6520
|
+
` readonly activeIdOrUndefined = computed(() => this.activeId() ?? undefined);`
|
|
6521
|
+
] : [
|
|
4710
6522
|
...hasEmbedded ? sampleSeedComment(isMock, " ", (literal) => [
|
|
4711
6523
|
`readonly messages = signal<ChatMessage[]>([${literal}]);`
|
|
4712
6524
|
]) : [],
|
|
@@ -4721,17 +6533,20 @@ function renderAngular(components, ctx) {
|
|
|
4721
6533
|
` private readonly sourcesEl = viewChild.required<ElementRef<KaiSourcesElement>>('sources');`
|
|
4722
6534
|
] : [];
|
|
4723
6535
|
const sourcesReapply = hasSourcesCompanion ? [` Object.assign(this.sourcesEl().nativeElement, { sources: this.sampleSources });`] : [];
|
|
4724
|
-
const chatTag = (pad) =>
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
6536
|
+
const chatTag = (pad) => {
|
|
6537
|
+
const slotted = block ? [] : slottedChildMarkup(components, " ", { wired: false, ids: false });
|
|
6538
|
+
return [
|
|
6539
|
+
`<kai-chat`,
|
|
6540
|
+
` #chat`,
|
|
6541
|
+
` [messages]="messages()"`,
|
|
6542
|
+
` [loading]="loading()"`,
|
|
6543
|
+
` [suggestions]="suggestions"`,
|
|
6544
|
+
` suggestion-mode="submit"`,
|
|
6545
|
+
` style="${p.chatFill}"`,
|
|
6546
|
+
` (kai-submit)="onSubmit($event)"`,
|
|
6547
|
+
...slotted.length === 0 ? [`></kai-chat>`] : [`>`, ...slotted, `</kai-chat>`]
|
|
6548
|
+
].map((l) => `${pad}${l}`);
|
|
6549
|
+
};
|
|
4735
6550
|
const templateBody = workspace ? [
|
|
4736
6551
|
` <!-- SCAF-14: workspace split — chat pane left, artifact preview right. -->`,
|
|
4737
6552
|
` <!-- kai-resizable needs kai-resizable-item children to render panels. -->`,
|
|
@@ -4747,7 +6562,39 @@ function renderAngular(components, ctx) {
|
|
|
4747
6562
|
// Siblings of the SPLIT — see the same line in renderJsx.
|
|
4748
6563
|
...companionLines
|
|
4749
6564
|
] : [...chatTag(" "), ...companionLines];
|
|
4750
|
-
const elementTypes =
|
|
6565
|
+
const elementTypes = [
|
|
6566
|
+
"KaiChatElement",
|
|
6567
|
+
...block ? ["KaiConversationsElement"] : [],
|
|
6568
|
+
...hasSourcesCompanion ? ["KaiSourcesElement"] : []
|
|
6569
|
+
].join(", ");
|
|
6570
|
+
const surfaceTemplate = block ? [
|
|
6571
|
+
` <!-- ── The workspace BLOCK: the shell + the rail + your thread records ── -->`,
|
|
6572
|
+
...htmlComment(
|
|
6573
|
+
[
|
|
6574
|
+
`<kai-workspace> is the chat-agnostic layout shell: header/start/main/`,
|
|
6575
|
+
`end/footer slots, aside resize + collapse, and a mobile drawer below`,
|
|
6576
|
+
`the drawer-below width. It knows nothing about chat.`
|
|
6577
|
+
],
|
|
6578
|
+
" "
|
|
6579
|
+
),
|
|
6580
|
+
` <kai-workspace drawer-below="720" style="display:block;width:100%;height:100%">`,
|
|
6581
|
+
...htmlComment(BLOCK_RAIL_NOTE, " "),
|
|
6582
|
+
` <!-- New chat = a DRAFT (activeId null): the record appears on the first turn. -->`,
|
|
6583
|
+
` <kai-conversations`,
|
|
6584
|
+
` #rail`,
|
|
6585
|
+
` slot="start"`,
|
|
6586
|
+
` [conversations]="rows()"`,
|
|
6587
|
+
` [activeId]="activeIdOrUndefined()"`,
|
|
6588
|
+
` style="display:block;height:100%"`,
|
|
6589
|
+
` (kai-conversation-select)="onSelect($event)"`,
|
|
6590
|
+
` (kai-new-chat)="onNewChat()"`,
|
|
6591
|
+
` ></kai-conversations>`,
|
|
6592
|
+
` <!-- Unnamed children project into the shell's MAIN region. -->`,
|
|
6593
|
+
` <div style="display:flex;flex-direction:column;height:100%;min-height:0">`,
|
|
6594
|
+
...indentEmitted([...attachmentTemplate, ...templateBody], " "),
|
|
6595
|
+
` </div>`,
|
|
6596
|
+
` </kai-workspace>`
|
|
6597
|
+
] : [...attachmentTemplate, ...templateBody];
|
|
4751
6598
|
return [
|
|
4752
6599
|
`// Angular standalone component — save as: src/app/chat.component.ts`,
|
|
4753
6600
|
`// Render it: put <app-chat /> in your root template and add ChatComponent to`,
|
|
@@ -4759,7 +6606,7 @@ function renderAngular(components, ctx) {
|
|
|
4759
6606
|
`// "styles": ["node_modules/@kitn.ai/ui/dist/theme.tokens.css", "src/styles.css"]`,
|
|
4760
6607
|
`// (@kitn.ai/ui/theme.tokens.css is the compiled token file; theme.css is`,
|
|
4761
6608
|
`// Tailwind source and is only for apps that compile Tailwind themselves.)`,
|
|
4762
|
-
`import { CUSTOM_ELEMENTS_SCHEMA, Component, ElementRef, afterNextRender, signal, viewChild } from '@angular/core';`,
|
|
6609
|
+
`import { CUSTOM_ELEMENTS_SCHEMA, Component, ElementRef, afterNextRender, ${block ? "computed, effect, " : ""}signal, viewChild } from '@angular/core';`,
|
|
4763
6610
|
`import '@kitn.ai/ui/elements'; // registers <kai-*> — required, must come first`,
|
|
4764
6611
|
`import type { ${elementTypes} } from '@kitn.ai/ui/elements';`,
|
|
4765
6612
|
...wireImportLines({
|
|
@@ -4768,7 +6615,8 @@ function renderAngular(components, ctx) {
|
|
|
4768
6615
|
cards: ctx.emitCards,
|
|
4769
6616
|
cardTools: ctx.cardProvider !== null,
|
|
4770
6617
|
mock: isMock,
|
|
4771
|
-
attachments
|
|
6618
|
+
attachments,
|
|
6619
|
+
workspaceBlock: block
|
|
4772
6620
|
}),
|
|
4773
6621
|
``,
|
|
4774
6622
|
`// ${ctx.label} — ${p.note}. empty-state hint: ${emptyHint}`,
|
|
@@ -4777,6 +6625,7 @@ function renderAngular(components, ctx) {
|
|
|
4777
6625
|
``,
|
|
4778
6626
|
...modelInit,
|
|
4779
6627
|
...attachmentModuleInit,
|
|
6628
|
+
...block ? [...blockThreadStoreLines(""), ``] : [],
|
|
4780
6629
|
...cardsInit,
|
|
4781
6630
|
...toolsLines,
|
|
4782
6631
|
...runnerLines,
|
|
@@ -4788,8 +6637,7 @@ function renderAngular(components, ctx) {
|
|
|
4788
6637
|
` schemas: [CUSTOM_ELEMENTS_SCHEMA],`,
|
|
4789
6638
|
` template: \``,
|
|
4790
6639
|
` <div style="${p.style}">`,
|
|
4791
|
-
...
|
|
4792
|
-
...templateBody,
|
|
6640
|
+
...surfaceTemplate,
|
|
4793
6641
|
` </div>`,
|
|
4794
6642
|
` \`,`,
|
|
4795
6643
|
`})`,
|
|
@@ -4797,12 +6645,17 @@ function renderAngular(components, ctx) {
|
|
|
4797
6645
|
` // Every write assigns a NEW array. That reference change is what re-renders`,
|
|
4798
6646
|
` // <kai-chat> — mutating the array in place does nothing.`,
|
|
4799
6647
|
...sampleSeed,
|
|
4800
|
-
` readonly loading = signal(false)
|
|
6648
|
+
...block ? [] : [` readonly loading = signal(false);`],
|
|
4801
6649
|
` readonly suggestions = ${jsArray(suggestions)};`,
|
|
4802
6650
|
...sourcesField,
|
|
4803
6651
|
` private readonly chatEl = viewChild.required<ElementRef<KaiChatElement>>('chat');`,
|
|
6652
|
+
...block ? [` private readonly railEl = viewChild.required<ElementRef<KaiConversationsElement>>('rail');`] : [],
|
|
4804
6653
|
``,
|
|
4805
6654
|
` constructor() {`,
|
|
6655
|
+
...block ? [
|
|
6656
|
+
` // The save scheduler rides an effect: every threads() write schedules a save.`,
|
|
6657
|
+
` effect(() => this.saver.schedule(this.threads()));`
|
|
6658
|
+
] : [],
|
|
4806
6659
|
` // SCAF-15: kai-* register via an async dynamic import (SSR-safety), so the`,
|
|
4807
6660
|
` // element may not be upgraded when Angular first applies the bindings above —`,
|
|
4808
6661
|
` // and a property set on a not-yet-upgraded element is dropped on upgrade.`,
|
|
@@ -4817,10 +6670,30 @@ function renderAngular(components, ctx) {
|
|
|
4817
6670
|
` suggestions: this.suggestions,`,
|
|
4818
6671
|
...cardPropAssign,
|
|
4819
6672
|
` });`,
|
|
6673
|
+
...block ? [
|
|
6674
|
+
` // The rail's array props follow the same upgrade rule.`,
|
|
6675
|
+
` await customElements.whenDefined('kai-conversations');`,
|
|
6676
|
+
` Object.assign(this.railEl().nativeElement, {`,
|
|
6677
|
+
` conversations: this.rows(),`,
|
|
6678
|
+
` activeId: this.activeIdOrUndefined(),`,
|
|
6679
|
+
` });`,
|
|
6680
|
+
` // Flush the pending write when the tab closes mid-debounce (or mid-stream).`,
|
|
6681
|
+
` window.addEventListener('beforeunload', () => this.saver.flush());`
|
|
6682
|
+
] : [],
|
|
4820
6683
|
...sourcesReapply,
|
|
4821
6684
|
` });`,
|
|
4822
6685
|
` }`,
|
|
4823
6686
|
``,
|
|
6687
|
+
...block ? [
|
|
6688
|
+
` onSelect(event: Event) {`,
|
|
6689
|
+
` this.activeId.set((event as CustomEvent<{ id: string }>).detail.id);`,
|
|
6690
|
+
` }`,
|
|
6691
|
+
` onNewChat() {`,
|
|
6692
|
+
` // A DRAFT, not a record: it becomes a thread on its first turn.`,
|
|
6693
|
+
` this.activeId.set(null);`,
|
|
6694
|
+
` }`,
|
|
6695
|
+
``
|
|
6696
|
+
] : [],
|
|
4824
6697
|
...attachmentFields,
|
|
4825
6698
|
``,
|
|
4826
6699
|
` // \`Event\`, not \`CustomEvent\`: under strictTemplates Angular types \`$event\` on`,
|
|
@@ -4835,13 +6708,17 @@ function renderAngular(components, ctx) {
|
|
|
4835
6708
|
}
|
|
4836
6709
|
function renderSolid(components, ctx) {
|
|
4837
6710
|
const { p, emptyHint, suggestions, isMock, defaultModel, emitTools, emitToolLoop } = ctx;
|
|
4838
|
-
const workspace =
|
|
6711
|
+
const workspace = isArtifactSplit(components);
|
|
6712
|
+
const block = isWorkspaceBlock(components);
|
|
4839
6713
|
const attachments = hasAttachments(components);
|
|
4840
6714
|
const standaloneCompanionTags = components.filter(
|
|
4841
|
-
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !
|
|
6715
|
+
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !ARTIFACT_SPLIT_TAGS.has(t) && !(block && BLOCK_TAGS.has(t)) && !(attachments && ATTACHMENT_TAGS.has(t))
|
|
4842
6716
|
);
|
|
4843
6717
|
const hasSources = standaloneCompanionTags.includes("kai-sources");
|
|
4844
6718
|
const hasVoice = standaloneCompanionTags.includes("kai-voice-input");
|
|
6719
|
+
const unrenderedCompanions = standaloneCompanionTags.filter(
|
|
6720
|
+
(t) => t !== "kai-sources" && t !== "kai-voice-input"
|
|
6721
|
+
);
|
|
4845
6722
|
const componentImports = [
|
|
4846
6723
|
"Attachment",
|
|
4847
6724
|
"AttachmentInfo",
|
|
@@ -4869,6 +6746,9 @@ function renderSolid(components, ctx) {
|
|
|
4869
6746
|
"SourceTrigger",
|
|
4870
6747
|
"Tool",
|
|
4871
6748
|
...workspace ? ["Artifact", "ResizableHandle", "ResizablePanel", "ResizablePanelGroup"] : [],
|
|
6749
|
+
// The BLOCK's structural pair: the Solid shell + the Solid rail (this target
|
|
6750
|
+
// renders components, not <kai-*> tags — same rule as everything above).
|
|
6751
|
+
...block ? ["ConversationList", "WorkspaceShell"] : [],
|
|
4872
6752
|
...hasVoice ? ["VoiceInput"] : [],
|
|
4873
6753
|
// `Attachments`/`Attachment`/`AttachmentPreview`/`AttachmentInfo` are already
|
|
4874
6754
|
// unconditional above — `renderPart` draws `file` PARTS with them whatever the
|
|
@@ -4880,17 +6760,32 @@ function renderSolid(components, ctx) {
|
|
|
4880
6760
|
const read = "messages()";
|
|
4881
6761
|
const commit = (value) => `setMessages(${value});`;
|
|
4882
6762
|
const setter = "(fn) => setMessages((prev) => fn(prev))";
|
|
6763
|
+
const blockRead = `(threads().find((t) => t.id === targetId)?.messages ?? [])`;
|
|
6764
|
+
const blockPrelude = block ? [
|
|
6765
|
+
...BLOCK_TARGET_ID_NOTE.map((l) => `// ${l}`),
|
|
6766
|
+
`const targetId = activeId() ?? crypto.randomUUID();`,
|
|
6767
|
+
`// The record is born on its first turn — with YOUR title policy.`,
|
|
6768
|
+
`setThreads((prev) => prev.some((t) => t.id === targetId)`,
|
|
6769
|
+
` ? prev`,
|
|
6770
|
+
` : [{ id: targetId, title: deriveTitle(value), updatedAt: new Date().toISOString(), messages: [] }, ...prev]);`,
|
|
6771
|
+
`setActiveId(targetId);`,
|
|
6772
|
+
...BLOCK_BIND_NOTE.map((l) => `// ${l}`),
|
|
6773
|
+
`const setMessages = bindThreadMessages<Thread>((fn) => { setThreads((prev) => fn(prev)); }, targetId, {`,
|
|
6774
|
+
` touch: (t) => ({ ...t, updatedAt: new Date().toISOString() }),`,
|
|
6775
|
+
`});`,
|
|
6776
|
+
`const controller = sessions.begin(targetId);`
|
|
6777
|
+
] : [];
|
|
4883
6778
|
const onSubmitBody = realStreamBody({
|
|
4884
6779
|
pad: " ",
|
|
4885
|
-
read,
|
|
4886
|
-
commitSet: (expr) => commit(expr),
|
|
4887
|
-
setterAdapter: setter,
|
|
4888
|
-
setLoading: (v2) => `setLoading(${v2});`,
|
|
6780
|
+
read: block ? blockRead : read,
|
|
6781
|
+
commitSet: block ? (expr) => `setMessages(() => ${expr});` : (expr) => commit(expr),
|
|
6782
|
+
setterAdapter: block ? "setMessages" : setter,
|
|
6783
|
+
setLoading: block ? (v2) => v2 === "true" ? `// loading for THIS thread now derives from the sessions map (streamingIds).` : `sessions.end(targetId, controller);` : (v2) => `setLoading(${v2});`,
|
|
4889
6784
|
bodyPayload: realBodyPayload({ defaultModel, tools: emitTools }),
|
|
4890
6785
|
strictRoles: true,
|
|
4891
6786
|
toolLoop: emitToolLoop,
|
|
4892
6787
|
cards: ctx.emitCards,
|
|
4893
|
-
thread: accessorThreadBinding(read, commit, setter),
|
|
6788
|
+
thread: block ? blockLiveThread(blockRead) : accessorThreadBinding(read, commit, setter),
|
|
4894
6789
|
valueSource: "input()",
|
|
4895
6790
|
// The staged files are captured BEFORE either list is cleared. `setInput('')`
|
|
4896
6791
|
// stays last so the order reads the way it executes.
|
|
@@ -4900,9 +6795,11 @@ function renderSolid(components, ctx) {
|
|
|
4900
6795
|
clear: ["setStaged([]);"],
|
|
4901
6796
|
fromSubmitEvent: false
|
|
4902
6797
|
}) : [],
|
|
6798
|
+
...blockPrelude,
|
|
4903
6799
|
`setInput('');`
|
|
4904
6800
|
],
|
|
4905
6801
|
...attachments ? { filesExpr: "files" } : {},
|
|
6802
|
+
...block ? { fetchSignal: "controller.signal" } : {},
|
|
4906
6803
|
mock: isMock
|
|
4907
6804
|
});
|
|
4908
6805
|
const modelInit = defaultModel ? [
|
|
@@ -5082,6 +6979,21 @@ function renderSolid(components, ctx) {
|
|
|
5082
6979
|
return [
|
|
5083
6980
|
`// SolidJS + Vite — save as: src/App.tsx`,
|
|
5084
6981
|
`//`,
|
|
6982
|
+
// What this target DROPS, said out loud. It renders Solid components for the
|
|
6983
|
+
// companions it knows (sources, voice input) and nothing at all for the rest,
|
|
6984
|
+
// so a `components` list naming one of those used to produce a file with no
|
|
6985
|
+
// trace of it — a silent drop, and the quiet half of the same defect the
|
|
6986
|
+
// conversation rail had. Naming them is not a fix; it is the honest record
|
|
6987
|
+
// until this target grows a branch for them.
|
|
6988
|
+
...unrenderedCompanions.length > 0 ? [
|
|
6989
|
+
`// NOT WIRED, and not rendered: ${unrenderedCompanions.join(", ")}.`,
|
|
6990
|
+
`// You asked for ${unrenderedCompanions.length === 1 ? "it" : "them"} and this target has no branch for`,
|
|
6991
|
+
`// ${unrenderedCompanions.length === 1 ? "it" : "them"} — see the note below on why this file renders Solid`,
|
|
6992
|
+
`// components rather than <kai-*> elements. Compose the Solid component`,
|
|
6993
|
+
`// yourself (ask the component_reference MCP tool for what it exports), or`,
|
|
6994
|
+
`// scaffold the html target, which drives the custom elements directly.`,
|
|
6995
|
+
`//`
|
|
6996
|
+
] : [],
|
|
5085
6997
|
`// This target does NOT use the <kai-*> custom elements, and that is deliberate:`,
|
|
5086
6998
|
`// the kit is AUTHORED in SolidJS, so a Solid app renders the real components`,
|
|
5087
6999
|
`// with real props and real fine-grained reactivity. Going through the`,
|
|
@@ -5098,7 +7010,7 @@ function renderSolid(components, ctx) {
|
|
|
5098
7010
|
`// The @source line is NOT optional: without it Tailwind scans only src/, strips`,
|
|
5099
7011
|
`// every kit utility class as unused, and the whole UI renders unstyled.`,
|
|
5100
7012
|
`// (theme.css here, not theme.tokens.css: this app compiles Tailwind itself.)`,
|
|
5101
|
-
`import { For, Index, Match, Show, Switch, createMemo, createSignal } from 'solid-js';`,
|
|
7013
|
+
`import { For, Index, Match, Show, Switch, ${block ? "createEffect, " : ""}createMemo, createSignal${block ? ", onCleanup" : ""} } from 'solid-js';`,
|
|
5102
7014
|
// '@kitn.ai/ui/solid', NOT the root '@kitn.ai/ui', and the difference is
|
|
5103
7015
|
// invisible to every compiler on this repo's critical path: src/solid.ts is
|
|
5104
7016
|
// `export * from './index'`, so ./solid is a strict SUPERSET of the root and
|
|
@@ -5123,13 +7035,15 @@ function renderSolid(components, ctx) {
|
|
|
5123
7035
|
toolLoop: emitToolLoop,
|
|
5124
7036
|
cards: ctx.emitCards,
|
|
5125
7037
|
cardTools: ctx.cardProvider !== null,
|
|
5126
|
-
mock: isMock
|
|
7038
|
+
mock: isMock,
|
|
7039
|
+
workspaceBlock: block
|
|
5127
7040
|
}),
|
|
5128
7041
|
``,
|
|
5129
7042
|
`// ${ctx.label} — ${p.note}. empty-state hint: ${emptyHint}`,
|
|
5130
7043
|
...(p.altNote ?? []).map((l) => `// ${l}`),
|
|
5131
7044
|
``,
|
|
5132
7045
|
...isMock ? [...mockResponderInit(), ``] : [],
|
|
7046
|
+
...block ? [...blockThreadStoreLines(""), ``] : [],
|
|
5133
7047
|
...cardsInit,
|
|
5134
7048
|
`// Narrow a part to ONE variant, or false. One read, one cast — and the JSX`,
|
|
5135
7049
|
`// below re-runs it on every delta, which is what keeps a growing text or`,
|
|
@@ -5303,8 +7217,34 @@ function renderSolid(components, ctx) {
|
|
|
5303
7217
|
` // streaming message as a NEW OBJECT on every delta: a new reference IS the`,
|
|
5304
7218
|
` // re-render signal. Which is exactly why the thread below is keyed by message`,
|
|
5305
7219
|
` // id rather than by the message objects — see messageKeys.`,
|
|
5306
|
-
|
|
5307
|
-
|
|
7220
|
+
...block ? [
|
|
7221
|
+
` // ── The block's state: your records + the kit's session/persistence helpers ─`,
|
|
7222
|
+
` const [threads, setThreads] = createSignal<Thread[]>(loadThreads());`,
|
|
7223
|
+
` // null means a DRAFT: "New chat" was pressed and nothing is stored yet. A`,
|
|
7224
|
+
` // record is created on its first turn, so the rail never fills with empties.`,
|
|
7225
|
+
` const [activeId, setActiveId] = createSignal<string | null>(threads()[0]?.id ?? null);`,
|
|
7226
|
+
` // One id per in-flight reply — a stream keeps running when you switch away.`,
|
|
7227
|
+
` const [streamingIds, setStreamingIds] = createSignal<string[]>([]);`,
|
|
7228
|
+
...BLOCK_SESSIONS_NOTE.map((l) => ` // ${l}`),
|
|
7229
|
+
` const sessions = createThreadSessions((ids) => setStreamingIds(ids));`,
|
|
7230
|
+
` // The save scheduler: the debounce/flush MECHANICS are the kit's; the delay,`,
|
|
7231
|
+
` // the save target and what a failed save means are YOURS (see saveThreads).`,
|
|
7232
|
+
` const saver = createSaveScheduler(saveThreads, { delayMs: 250 });`,
|
|
7233
|
+
` createEffect(() => saver.schedule(threads()));`,
|
|
7234
|
+
` // Flush the pending write when the tab closes mid-debounce (or mid-stream).`,
|
|
7235
|
+
` const flushOnUnload = () => saver.flush();`,
|
|
7236
|
+
` window.addEventListener('beforeunload', flushOnUnload);`,
|
|
7237
|
+
` onCleanup(() => window.removeEventListener('beforeunload', flushOnUnload));`,
|
|
7238
|
+
` const active = createMemo(() => threads().find((t) => t.id === activeId()) ?? null);`,
|
|
7239
|
+
` const messages = createMemo(() => active()?.messages ?? []);`,
|
|
7240
|
+
` const loading = createMemo(() => { const id = activeId(); return id !== null && streamingIds().includes(id); });`,
|
|
7241
|
+
` // The rail's row shape is a VIEW of your record — titles and counts, no`,
|
|
7242
|
+
` // message bodies. The rail renders the array in the order it is given.`,
|
|
7243
|
+
` const rows = createMemo(() => threads().map((t) => ({ id: t.id, title: t.title, messageCount: t.messages.length, updatedAt: t.updatedAt })));`
|
|
7244
|
+
] : [
|
|
7245
|
+
` const [messages, setMessages] = createSignal<ChatMessage[]>([]);`,
|
|
7246
|
+
` const [loading, setLoading] = createSignal(false);`
|
|
7247
|
+
],
|
|
5308
7248
|
` // PromptInput is CONTROLLED here, so this signal — not a kai-submit event — is`,
|
|
5309
7249
|
` // where the submitted text comes from.`,
|
|
5310
7250
|
` const [input, setInput] = createSignal('');`,
|
|
@@ -5332,7 +7272,31 @@ function renderSolid(components, ctx) {
|
|
|
5332
7272
|
` // component below it. Mount it once, at the top.`,
|
|
5333
7273
|
` <ChatConfig>`,
|
|
5334
7274
|
` <div style={{ ${solidStyle(p.style)} }}>`,
|
|
5335
|
-
...
|
|
7275
|
+
...block ? [
|
|
7276
|
+
` {/* ── The workspace BLOCK: the shell + the rail + your thread records ── */}`,
|
|
7277
|
+
` {/* WorkspaceShell is the chat-agnostic layout shell (header/start/main/`,
|
|
7278
|
+
` end/footer regions, aside resize + collapse, a mobile drawer below`,
|
|
7279
|
+
` drawerBelow px). It knows nothing about chat. */}`,
|
|
7280
|
+
` <WorkspaceShell`,
|
|
7281
|
+
` drawerBelow={720}`,
|
|
7282
|
+
` class="h-full w-full"`,
|
|
7283
|
+
` start={`,
|
|
7284
|
+
// A JS line comment, not a {/* */} pair: inside an attribute's
|
|
7285
|
+
// expression container the braces form is a parse error.
|
|
7286
|
+
...BLOCK_RAIL_NOTE_SOLID.map((l) => ` // ${l}`),
|
|
7287
|
+
` <ConversationList`,
|
|
7288
|
+
` groups={[]}`,
|
|
7289
|
+
` conversations={rows()}`,
|
|
7290
|
+
` activeId={activeId() ?? undefined}`,
|
|
7291
|
+
` onSelect={(id) => setActiveId(id)}`,
|
|
7292
|
+
` onNewChat={() => setActiveId(null)}`,
|
|
7293
|
+
` class="h-full"`,
|
|
7294
|
+
` />`,
|
|
7295
|
+
` }`,
|
|
7296
|
+
` >`,
|
|
7297
|
+
...indentEmitted(tree, " "),
|
|
7298
|
+
` </WorkspaceShell>`
|
|
7299
|
+
] : tree,
|
|
5336
7300
|
` </div>`,
|
|
5337
7301
|
` </ChatConfig>`,
|
|
5338
7302
|
` );`,
|
|
@@ -5480,17 +7444,38 @@ function viteMiddlewareAdapter(plugin) {
|
|
|
5480
7444
|
` name: 'chat-api',`,
|
|
5481
7445
|
` configureServer(server) {`,
|
|
5482
7446
|
` server.middlewares.use('/api/chat', async (req, res) => {`,
|
|
7447
|
+
` // The METHOD has to survive the bridge, like the status does below.`,
|
|
7448
|
+
` // Hard-coding 'POST' here meant a bare GET /api/chat arrived at`,
|
|
7449
|
+
` // readChatRequest as an empty POST and came back 400 "not valid JSON":`,
|
|
7450
|
+
` // the 405 guard was emitted, compiled, and unreachable on this host.`,
|
|
7451
|
+
` const method = req.method ?? 'POST';`,
|
|
7452
|
+
` // undici THROWS on \`new Request(url, { method: 'GET', body })\`, so only`,
|
|
7453
|
+
` // read a body from the methods that can carry one.`,
|
|
7454
|
+
` const hasBody = method !== 'GET' && method !== 'HEAD';`,
|
|
5483
7455
|
` let body = '';`,
|
|
5484
|
-
`
|
|
5485
|
-
`
|
|
7456
|
+
` if (hasBody) {`,
|
|
7457
|
+
` req.setEncoding('utf8');`,
|
|
7458
|
+
` for await (const chunk of req) body += chunk;`,
|
|
7459
|
+
` }`,
|
|
5486
7460
|
``,
|
|
5487
|
-
`
|
|
5488
|
-
`
|
|
5489
|
-
`
|
|
5490
|
-
`
|
|
5491
|
-
`
|
|
5492
|
-
`
|
|
5493
|
-
`
|
|
7461
|
+
` let response: Response;`,
|
|
7462
|
+
` try {`,
|
|
7463
|
+
` response = await chatHandler(`,
|
|
7464
|
+
` new Request('http://localhost/api/chat', {`,
|
|
7465
|
+
` method,`,
|
|
7466
|
+
` headers: { 'Content-Type': 'application/json' },`,
|
|
7467
|
+
` body: hasBody ? body : undefined,`,
|
|
7468
|
+
` }),`,
|
|
7469
|
+
` );`,
|
|
7470
|
+
` } catch (error) {`,
|
|
7471
|
+
` // An unhandled rejection in an async connect middleware EXITS Node 22 —`,
|
|
7472
|
+
` // this catch is the guard findings F-10 exists for. The browser gets`,
|
|
7473
|
+
` // eight generic words, so the TERMINAL has to get the real thing:`,
|
|
7474
|
+
` // swallowing it here would make a dev server strictly less`,
|
|
7475
|
+
` // diagnosable than the crash it replaced.`,
|
|
7476
|
+
` console.error('[chat-api] chatHandler failed:', error);`,
|
|
7477
|
+
` response = Response.json({ error: 'Chat handler failed.' }, { status: 500 });`,
|
|
7478
|
+
` }`,
|
|
5494
7479
|
``,
|
|
5495
7480
|
` // The STATUS has to survive the bridge: a 401 from the provider that`,
|
|
5496
7481
|
` // arrives at the browser as a 200 is a blank bubble and no error.`,
|
|
@@ -5807,7 +7792,13 @@ function cannotHostWarning(integration, route, framework) {
|
|
|
5807
7792
|
// and reaches localhost, so the page can call it. That advice was emitted
|
|
5808
7793
|
// identically for every integration because nothing read the flag that
|
|
5809
7794
|
// distinguishes them.
|
|
5810
|
-
...integration.
|
|
7795
|
+
...integration.id === "mock" ? [
|
|
7796
|
+
// For mock the honest first option is to do nothing: the block is
|
|
7797
|
+
// already labelled OPTIONAL, and "call it directly" would be advice
|
|
7798
|
+
// about an upstream that does not exist.
|
|
7799
|
+
`# • or ignore it: the front end above already streams the mock locally, so`,
|
|
7800
|
+
`# nothing in this scaffold needs the route to run.`
|
|
7801
|
+
] : integration.keyExposure === "frontend-safe" ? [
|
|
5811
7802
|
`# • or drop the route entirely: this integration needs no server hop — see the`,
|
|
5812
7803
|
`# run note — so the page may call it directly (CORS applies).`
|
|
5813
7804
|
] : [],
|
|
@@ -5816,7 +7807,8 @@ function cannotHostWarning(integration, route, framework) {
|
|
|
5816
7807
|
`# • it is a separate SERVICE either way: run it (uvicorn main:app) and proxy`,
|
|
5817
7808
|
`# /api/chat to it, or point the fetch at http://localhost:8000/api/chat.`
|
|
5818
7809
|
] : [`# • or run it where it belongs: framework: "${route.framework}".`],
|
|
5819
|
-
|
|
7810
|
+
// Not emitted for mock itself: "use mock" is where the reader already is.
|
|
7811
|
+
...integration.id === "mock" ? [] : [`# • or use integration: "mock" for a zero-config local stream (no backend, no key).`]
|
|
5820
7812
|
];
|
|
5821
7813
|
return [
|
|
5822
7814
|
`#`,
|
|
@@ -5922,7 +7914,7 @@ function compose(components, integration, placement, framework, suggestions, aud
|
|
|
5922
7914
|
audience
|
|
5923
7915
|
});
|
|
5924
7916
|
const isMock = integration.id === "mock";
|
|
5925
|
-
const route =
|
|
7917
|
+
const route = chooseRoute(integration, framework);
|
|
5926
7918
|
const header = [
|
|
5927
7919
|
`# AI/UI scaffold — ${preset ? preset.title : surfaceLabel(components)} × ${integration.title}`,
|
|
5928
7920
|
`combo: ${preset ? preset.id : components.join("+")} × ${integration.id} × ${placement} × ${framework}`,
|
|
@@ -5936,11 +7928,17 @@ function compose(components, integration, placement, framework, suggestions, aud
|
|
|
5936
7928
|
const block2Parts = [`=== (2) BACKEND ROUTE ===`, ``];
|
|
5937
7929
|
if (isMock) {
|
|
5938
7930
|
block2Parts.push(
|
|
5939
|
-
`#
|
|
5940
|
-
`#
|
|
5941
|
-
`#
|
|
7931
|
+
`# OPTIONAL — the front end above streams its reply locally, so this scaffold`,
|
|
7932
|
+
`# RUNS with no backend and no key. The route below is the mock's server half:`,
|
|
7933
|
+
`# the same createMockResponder() frames served over HTTP, so you can stand up`,
|
|
7934
|
+
`# the real /api/chat seam before any provider exists. To use it, swap the`,
|
|
7935
|
+
`# onSubmit's \`const res = mockResponse(value);\` for a request to /api/chat and`,
|
|
7936
|
+
`# hand its response body to the same reader. Going live later then means`,
|
|
7937
|
+
`# editing THIS file only — replace the responder with a provider call, or`,
|
|
7938
|
+
`# re-scaffold with a real integration and this block becomes its route.`
|
|
5942
7939
|
);
|
|
5943
|
-
}
|
|
7940
|
+
}
|
|
7941
|
+
if (route) {
|
|
5944
7942
|
if (!route.exact) {
|
|
5945
7943
|
block2Parts.push(
|
|
5946
7944
|
`# Note: ${integration.title} has no route for "${framework}". Emitting its native`,
|
|
@@ -6142,8 +8140,8 @@ function rejectUseCase(id) {
|
|
|
6142
8140
|
// so the rejection that teaches the id list is the right place to say so.
|
|
6143
8141
|
`These are PRESETS over the real axis, which is \`components\`. To compose a surface no`,
|
|
6144
8142
|
`preset names, pass the list directly, e.g. components: ["kai-chat", "kai-tool",`,
|
|
6145
|
-
`"kai-reasoning", "kai-artifact", "kai-resizable"] for
|
|
6146
|
-
`tool calls. Pick a preset id or pass \`components\`, then call scaffold again.`
|
|
8143
|
+
`"kai-reasoning", "kai-artifact", "kai-resizable"] for an artifact split that also renders`,
|
|
8144
|
+
`its tool calls. Pick a preset id or pass \`components\`, then call scaffold again.`
|
|
6147
8145
|
].join("\n");
|
|
6148
8146
|
}
|
|
6149
8147
|
const scaffold = {
|
|
@@ -6207,60 +8205,99 @@ const scaffold = {
|
|
|
6207
8205
|
);
|
|
6208
8206
|
}
|
|
6209
8207
|
};
|
|
8208
|
+
const themeCss = "/* @kitn.ai/ui design system — import after `@import \"tailwindcss\"`.\n Defines ONLY namespaced (--color-*) tokens, component keyframes, animation\n utilities, and the .chat-markdown styles. Contains NO global base/reset rules,\n so importing it restyles nothing on its own — a host opts in via the kit's\n components + classes. Rebrand by overriding the --color-* values (optionally\n scoped to a subtree).\n NOTE: This file is Tailwind-v4 source (requires tailwindcss as a peer). For\n bundler-based apps that don't use Tailwind, import @kitn.ai/ui/theme.tokens.css\n instead — it is plain compiled CSS with no toolchain requirements. */\n\n@custom-variant dark (&:is(.dark *));\n\n@theme {\n --color-background: var(--kai-color-background, hsl(0 0% 100%));\n --color-foreground: var(--kai-color-foreground, hsl(240 10% 3.9%));\n --color-card: var(--kai-color-card, hsl(0 0% 100%));\n --color-card-foreground: var(--kai-color-card-foreground, hsl(240 10% 3.9%));\n --color-popover: var(--kai-color-popover, hsl(0 0% 100%));\n --color-popover-foreground: var(--kai-color-popover-foreground, hsl(240 10% 3.9%));\n --color-primary: var(--kai-color-primary, hsl(240 5.9% 10%));\n --color-primary-foreground: var(--kai-color-primary-foreground, hsl(0 0% 98%));\n --color-secondary: var(--kai-color-secondary, hsl(240 4.8% 95.9%));\n --color-secondary-foreground: var(--kai-color-secondary-foreground, hsl(240 5.9% 10%));\n --color-muted: var(--kai-color-muted, hsl(240 4.8% 95.9%));\n --color-muted-foreground: var(--kai-color-muted-foreground, hsl(240 3.8% 43%));\n /* Subtle SURFACE — an opaque \"muted panel\" for things that sit OVER content\n (the prompt input, message bubbles, artifact/card headers, chips). CALCULATED\n from existing tokens — `--color-muted` blended 40% onto `--color-background`\n — so it tracks the theme (incl. dark) automatically and needs no hand-picked\n value. SOLID (no alpha), so nothing behind it bleeds through. This replaces\n ad-hoc `bg-muted/40` surfaces. Retint via the --kai-color-surface knob (or a\n bare value); --color-surface-strong is the stronger step (hover/raised) and\n --color-surface-sunken is recessed BELOW the base surface (wells, or a banner\n that sits behind a card). Sunken is theme-specific: \"darker than the card\"\n means a LOWER muted% in dark but a HIGHER one in light, so the two scopes\n can't share a number — see the .dark block. */\n --color-surface: var(--kai-color-surface, color-mix(in srgb, var(--color-muted) 40%, var(--color-background)));\n --color-surface-strong: var(--kai-color-surface-strong, color-mix(in srgb, var(--color-muted) 60%, var(--color-background)));\n --color-surface-sunken: var(--kai-color-surface-sunken, color-mix(in srgb, var(--color-muted) 85%, var(--color-background)));\n --color-accent: var(--kai-color-accent, hsl(240 4.8% 95.9%));\n --color-accent-foreground: var(--kai-color-accent-foreground, hsl(240 5.9% 10%));\n --color-destructive: var(--kai-color-destructive, hsl(0 72% 45%));\n --color-destructive-foreground: var(--kai-color-destructive-foreground, hsl(0 0% 98%));\n /* Semantic status colors - solid fills with a legible foreground, matched to\n the kit's tool-green / -amber / -blue hues so they read on-brand. Use\n bg-success / bg-warning / bg-info with the matching -foreground for badges,\n banners, and buttons; or text-success / -warning / -info on a soft tint\n (below). destructive (above) is the danger/error member of the same family.\n Light uses darker hues so the color carries a light foreground AND stays\n legible as text on a pale tint; dark (see .dark) flips to brighter hues over\n a near-black foreground. Override via --kai-color-success / -warning / -info\n (+ -foreground). */\n --color-success: var(--kai-color-success, hsl(142 71% 30%));\n --color-success-foreground: var(--kai-color-success-foreground, hsl(0 0% 98%));\n --color-warning: var(--kai-color-warning, hsl(38 92% 30%));\n --color-warning-foreground: var(--kai-color-warning-foreground, hsl(0 0% 98%));\n --color-info: var(--kai-color-info, hsl(217 91% 42%));\n --color-info-foreground: var(--kai-color-info-foreground, hsl(0 0% 98%));\n /* Soft tinted status surfaces - OPAQUE, theme-tracking fills derived as ~14% of\n the status hue blended onto the background. Use these for subtle callouts /\n banners instead of an eyeballed bg-success/10; pair with text-success /\n -warning / -info / -destructive. Derived, so declared in BOTH light and dark\n (a color-mix resolves where it's declared - see the surface tokens above). A\n bare bg- utility for a color-mix token is added at the bottom (see bg-surface). */\n --color-success-soft: var(--kai-color-success-soft, color-mix(in oklab, var(--color-success) 14%, var(--color-background)));\n --color-warning-soft: var(--kai-color-warning-soft, color-mix(in oklab, var(--color-warning) 14%, var(--color-background)));\n --color-info-soft: var(--kai-color-info-soft, color-mix(in oklab, var(--color-info) 14%, var(--color-background)));\n --color-destructive-soft: var(--kai-color-destructive-soft, color-mix(in oklab, var(--color-destructive) 14%, var(--color-background)));\n /* Interaction-state backgrounds - opaque, theme-tracking fills for row/control\n states so consumers stop eyeballing /10 opacities. --color-hover is a subtle\n neutral (a little --color-muted on the background); --color-selected is a\n faint --color-primary tint so a selected row tracks the brand. Use bg-hover /\n bg-selected. Derived, so declared in BOTH light and dark; bare bg- utilities\n added at the bottom. */\n --color-hover: var(--kai-color-hover, color-mix(in oklab, var(--color-muted) 60%, var(--color-background)));\n --color-selected: var(--kai-color-selected, color-mix(in oklab, var(--color-primary) 8%, var(--color-background)));\n --color-border: var(--kai-color-border, hsl(240 5.9% 90%));\n --color-input: var(--kai-color-input, hsl(240 5.9% 90%));\n /* Focus ring — a deliberate blue (not a neutral) so keyboard focus is always\n obvious and on-brand in both modes. Light uses a strong blue; dark uses a\n brighter one for contrast on dark surfaces. Both clear WCAG 2.1 non-text\n contrast (≥3:1) against the kit's backgrounds. Override via --kai-color-ring. */\n --color-ring: var(--kai-color-ring, hsl(217 91% 53%));\n /* Custom scrollbars — thin, rounded, subtle; thumb strengthens on hover. A\n consistent cross-platform look (the kit styles its OWN scroll regions inside\n the shadow root; the track stays transparent so nothing is forced where no\n scrollbar is needed). Override via --kai-color-scrollbar-thumb(-hover). */\n --color-scrollbar-thumb: var(--kai-color-scrollbar-thumb, hsl(240 5% 80%));\n --color-scrollbar-thumb-hover: var(--kai-color-scrollbar-thumb-hover, hsl(240 4% 64%));\n --color-sidebar: var(--kai-color-sidebar, hsl(0 0% 100%));\n\n /* Inline `code` accent. Blue text on a translucent blue chip. */\n --color-code-foreground: var(--kai-color-code-foreground, hsl(224.3 76.3% 48%));\n\n /* Tool/status chip hues. Each chip is hue text over a 15% translucent fill of\n the SAME hue (set in tool.tsx). The bare hue is too light to reach WCAG AA\n (4.5:1) on the faint fill in LIGHT mode, so the light values are darkened;\n dark mode keeps brighter hues for AA on the dark fill. Override via\n --kai-color-tool-* to retheme. */\n --color-tool-blue: var(--kai-color-tool-blue, hsl(217 91% 38%));\n --color-tool-amber: var(--kai-color-tool-amber, hsl(38 92% 28%));\n --color-tool-green: var(--kai-color-tool-green, hsl(142 71% 26%));\n --color-tool-red: var(--kai-color-tool-red, hsl(0 72% 42%));\n\n --radius: var(--kai-radius, 0.6rem);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n\n /* Typography scale — semantic sizes shared by all components. Each generates a\n Tailwind utility (text-caption / text-meta / text-body / text-title). To\n restyle the kit's typography, override the namespaced --kai-text-* token on\n :root (e.g. `--kai-text-body: 0.9375rem`) — it pierces the Shadow DOM via the\n var() fallback, exactly like the --kai-color-* tokens. The bare --text-*\n names stay internal so a host's own --text-* can't collide. (Message/markdown/\n input reading size also scales with the `proseSize` prop; these tokens cover\n the fixed chrome & controls.) */\n --text-caption: var(--kai-text-caption, 0.6875rem); --text-caption--line-height: 1rem; /* 11px — micro labels, badges, sub-counts */\n --text-meta: var(--kai-text-meta, 0.75rem); --text-meta--line-height: 1.1rem; /* 12px — controls, toggles, switchers, captions */\n --text-body: var(--kai-text-body, 0.875rem); --text-body--line-height: 1.45rem; /* 14px — primary reading text */\n --text-title: var(--kai-text-title, 1rem); --text-title--line-height: 1.5rem; /* 16px — emphasis / headers */\n\n @keyframes typing { 0%,100% { transform: translateY(0); opacity: .5 } 50% { transform: translateY(-2px); opacity: 1 } }\n @keyframes loading-dots { 0%,100% { opacity: 0 } 50% { opacity: 1 } }\n @keyframes wave { 0%,100% { transform: scaleY(1) } 50% { transform: scaleY(.6) } }\n @keyframes blink { 0%,100% { opacity: 1 } 50% { opacity: 0 } }\n @keyframes text-blink { 0%,100% { color: var(--color-primary) } 50% { color: var(--color-muted-foreground) } }\n @keyframes bounce-dots { 0%,100% { transform: scale(.8); opacity: .5 } 50% { transform: scale(1.2); opacity: 1 } }\n @keyframes thin-pulse { 0%,100% { transform: scale(.95); opacity: .8 } 50% { transform: scale(1.05); opacity: .4 } }\n @keyframes pulse-dot { 0%,100% { transform: scale(1); opacity: .8 } 50% { transform: scale(1.5); opacity: 1 } }\n @keyframes shimmer-text { 0% { background-position: 150% center } 100% { background-position: -150% center } }\n @keyframes wave-bars { 0%,100% { transform: scaleY(1); opacity: .5 } 50% { transform: scaleY(.6); opacity: 1 } }\n @keyframes shimmer { 0% { background-position: 200% 50% } 100% { background-position: -200% 50% } }\n @keyframes spinner-fade { 0% { opacity: 0 } 100% { opacity: 1 } }\n @keyframes collapsible-down { from { height: 0; opacity: 0 } to { height: var(--kb-collapsible-content-height); opacity: 1 } }\n @keyframes collapsible-up { from { height: var(--kb-collapsible-content-height); opacity: 1 } to { height: 0; opacity: 0 } }\n}\n\n.dark {\n --color-background: var(--kai-color-background, hsl(50 2% 9%));\n --color-foreground: var(--kai-color-foreground, hsl(0 0% 98%));\n --color-card: var(--kai-color-card, hsl(45 4% 12%));\n --color-card-foreground: var(--kai-color-card-foreground, hsl(0 0% 98%));\n --color-popover: var(--kai-color-popover, hsl(45 4% 12%));\n --color-popover-foreground: var(--kai-color-popover-foreground, hsl(0 0% 98%));\n --color-primary: var(--kai-color-primary, hsl(0 0% 98%));\n --color-primary-foreground: var(--kai-color-primary-foreground, hsl(45 4% 11%));\n --color-secondary: var(--kai-color-secondary, hsl(45 4% 17%));\n --color-secondary-foreground: var(--kai-color-secondary-foreground, hsl(0 0% 98%));\n --color-muted: var(--kai-color-muted, hsl(45 4% 17%));\n --color-muted-foreground: var(--kai-color-muted-foreground, hsl(45 4% 64%));\n /* Re-declared in the dark scope so the color-mix recomputes with the DARK\n muted/background (a derived token is computed where it's declared, not where\n it's used — see the light --color-surface for the full note). */\n --color-surface: var(--kai-color-surface, color-mix(in srgb, var(--color-muted) 40%, var(--color-background)));\n --color-surface-strong: var(--kai-color-surface-strong, color-mix(in srgb, var(--color-muted) 60%, var(--color-background)));\n /* Sunken: in dark a LOWER muted% is darker (blends toward the dark background). */\n --color-surface-sunken: var(--kai-color-surface-sunken, color-mix(in srgb, var(--color-muted) 18%, var(--color-background)));\n --color-accent: var(--kai-color-accent, hsl(45 4% 17%));\n --color-accent-foreground: var(--kai-color-accent-foreground, hsl(0 0% 98%));\n --color-destructive: var(--kai-color-destructive, hsl(0 62.8% 30.6%));\n --color-destructive-foreground: var(--kai-color-destructive-foreground, hsl(0 0% 98%));\n /* Semantic status colors - dark mode flips to the brighter tool-* dark hues\n (matching the chip values) carried over a near-black foreground, so the color\n both reads as text on a dark soft tint AND backs a legible foreground. See\n the light @theme note. */\n --color-success: var(--kai-color-success, hsl(142 71% 45%));\n --color-success-foreground: var(--kai-color-success-foreground, hsl(45 4% 11%));\n --color-warning: var(--kai-color-warning, hsl(38 92% 50%));\n --color-warning-foreground: var(--kai-color-warning-foreground, hsl(45 4% 11%));\n --color-info: var(--kai-color-info, hsl(217 91% 70%));\n --color-info-foreground: var(--kai-color-info-foreground, hsl(45 4% 11%));\n /* Soft status surfaces + interaction states - re-declared so each color-mix\n recomputes with the DARK hues/background (a derived token resolves where it's\n declared; see the light block for the full note). */\n --color-success-soft: var(--kai-color-success-soft, color-mix(in oklab, var(--color-success) 14%, var(--color-background)));\n --color-warning-soft: var(--kai-color-warning-soft, color-mix(in oklab, var(--color-warning) 14%, var(--color-background)));\n --color-info-soft: var(--kai-color-info-soft, color-mix(in oklab, var(--color-info) 14%, var(--color-background)));\n --color-destructive-soft: var(--kai-color-destructive-soft, color-mix(in oklab, var(--color-destructive) 14%, var(--color-background)));\n --color-hover: var(--kai-color-hover, color-mix(in oklab, var(--color-muted) 60%, var(--color-background)));\n --color-selected: var(--kai-color-selected, color-mix(in oklab, var(--color-primary) 8%, var(--color-background)));\n --color-border: var(--kai-color-border, hsl(45 4% 17%));\n --color-input: var(--kai-color-input, hsl(45 4% 17%));\n --color-ring: var(--kai-color-ring, hsl(217 91% 68%));\n --color-scrollbar-thumb: var(--kai-color-scrollbar-thumb, hsl(45 3% 30%));\n --color-scrollbar-thumb-hover: var(--kai-color-scrollbar-thumb-hover, hsl(45 3% 42%));\n --color-sidebar: var(--kai-color-sidebar, hsl(50 2% 7%));\n --color-code-foreground: var(--kai-color-code-foreground, hsl(213 94% 78%));\n\n /* Tool/status chip hues — dark mode. Brighter hues reach AA on the dark fill. */\n --color-tool-blue: var(--kai-color-tool-blue, hsl(217 91% 70%));\n --color-tool-amber: var(--kai-color-tool-amber, hsl(38 92% 50%));\n --color-tool-green: var(--kai-color-tool-green, hsl(142 71% 45%));\n --color-tool-red: var(--kai-color-tool-red, hsl(0 84% 70%));\n}\n\n/* Self-contained markdown styling — replaces the typography plugin's `prose`.\n Keyed on --color-* tokens so it themes with the rest of the kit. */\n.chat-markdown { color: var(--color-foreground); line-height: 1.6; }\n.chat-markdown > div:first-child > :first-child { margin-top: 0; }\n.chat-markdown > div:last-child > :last-child { margin-bottom: 0; }\n.chat-markdown p { margin: 0 0 0.75em; }\n.chat-markdown p:last-child { margin-bottom: 0; }\n.chat-markdown h1, .chat-markdown h2, .chat-markdown h3, .chat-markdown h4 {\n margin: 1.2em 0 0.5em; font-weight: 600; line-height: 1.3; color: var(--color-foreground);\n}\n.chat-markdown h1 { font-size: 1.4em; } .chat-markdown h2 { font-size: 1.25em; }\n.chat-markdown h3 { font-size: 1.1em; } .chat-markdown h4 { font-size: 1em; }\n.chat-markdown ul, .chat-markdown ol { margin: 0 0 0.75em; padding-left: 1.4em; }\n.chat-markdown li { margin: 0.2em 0; }\n.chat-markdown ul { list-style: disc; } .chat-markdown ol { list-style: decimal; }\n.chat-markdown a { color: var(--color-primary); text-decoration: underline; text-underline-offset: 2px; }\n.chat-markdown strong { font-weight: 600; color: var(--color-foreground); }\n.chat-markdown em { font-style: italic; }\n.chat-markdown blockquote {\n margin: 0 0 0.75em; padding-left: 0.9em; border-left: 2px solid var(--color-border);\n color: var(--color-muted-foreground);\n}\n.chat-markdown hr { margin: 1em 0; border: 0; border-top: 1px solid var(--color-border); }\n.chat-markdown code:not(pre code) {\n background: color-mix(in oklab, var(--color-code-foreground) 15%, transparent);\n color: var(--color-code-foreground);\n border-radius: 4px; padding: 0.1em 0.35em; font-size: 0.875em;\n font-family: var(--kai-font-code, ui-monospace, \"SF Mono\", Menlo, monospace);\n}\n.chat-markdown table { width: 100%; border-collapse: collapse; margin: 0 0 0.75em; font-size: 0.9em; }\n.chat-markdown th, .chat-markdown td { border: 1px solid var(--color-border); padding: 0.4em 0.6em; text-align: left; }\n.chat-markdown th { font-weight: 600; background: color-mix(in oklab, var(--color-muted-foreground) 8%, transparent); }\n\n/* Themeable elevation. The kit is mostly flat (borders), but a few surfaces lift\n off the page — popovers, dropdowns, hover cards, cards. The shadow COLOR is a\n token (--kai-shadow-color, default near-black); set it transparent for a fully\n flat look, or tint it to match a theme. Defined here (not as a Tailwind shadow\n utility) so the color stays overridable and works in both the web-component and\n SolidJS layers. */\n.kai-elevation {\n box-shadow:\n 0 4px 12px -2px color-mix(in oklab, var(--kai-shadow-color, oklch(0 0 0)) 14%, transparent),\n 0 2px 6px -2px color-mix(in oklab, var(--kai-shadow-color, oklch(0 0 0)) 10%, transparent);\n}\n.kai-elevation-sm {\n box-shadow: 0 1px 2px 0 color-mix(in oklab, var(--kai-shadow-color, oklch(0 0 0)) 8%, transparent);\n}\n\n/* Cross-platform thin scrollbar utility (Firefox + WebKit). Used by ScrollArea\n and any scroll region in the SolidJS-component build (web components also get\n this globally inside their shadow root — see src/elements/styles.css). */\n.scrollbar-thin {\n scrollbar-width: thin;\n scrollbar-color: var(--color-scrollbar-thumb) transparent;\n}\n.scrollbar-thin::-webkit-scrollbar { width: 8px; height: 8px; }\n.scrollbar-thin::-webkit-scrollbar-track { background: transparent; }\n.scrollbar-thin::-webkit-scrollbar-thumb {\n background-color: var(--color-scrollbar-thumb);\n border-radius: 9999px;\n border: 2px solid transparent;\n background-clip: padding-box;\n}\n.scrollbar-thin::-webkit-scrollbar-thumb:hover { background-color: var(--color-scrollbar-thumb-hover); }\n.scrollbar-thin::-webkit-scrollbar-corner { background: transparent; }\n\n/* Surface utilities — `bg-surface` / `bg-surface-strong` paint the opaque,\n theme-derived surface tokens (see --color-surface above). Defined explicitly\n because a color-mix-valued @theme token does NOT emit a bare `bg-` utility on\n its own (only opacity variants). Use these for panels that sit OVER content\n instead of `bg-muted/NN`. */\n@utility bg-surface {\n background-color: var(--color-surface);\n}\n@utility bg-surface-strong {\n background-color: var(--color-surface-strong);\n}\n@utility bg-surface-sunken {\n background-color: var(--color-surface-sunken);\n}\n\n/* Soft-status + interaction-state utilities. Same reason as bg-surface above: a\n color-mix-valued @theme token does NOT emit a bare `bg-` utility on its own, so\n bg-success-soft / bg-warning-soft / bg-info-soft / bg-destructive-soft and\n bg-hover / bg-selected are declared here. (The SOLID tokens - success / warning\n / info + their -foreground - are plain hsl values, so bg-success / text-success\n etc. ARE auto-generated by @theme and need no explicit utility.) */\n@utility bg-success-soft {\n background-color: var(--color-success-soft);\n}\n@utility bg-warning-soft {\n background-color: var(--color-warning-soft);\n}\n@utility bg-info-soft {\n background-color: var(--color-info-soft);\n}\n@utility bg-destructive-soft {\n background-color: var(--color-destructive-soft);\n}\n@utility bg-hover {\n background-color: var(--color-hover);\n}\n@utility bg-selected {\n background-color: var(--color-selected);\n}\n";
|
|
8209
|
+
function declaredKaiTokens(css) {
|
|
8210
|
+
const names = /* @__PURE__ */ new Set();
|
|
8211
|
+
for (const m of css.matchAll(/var\(\s*(--kai-[a-zA-Z0-9-]+)\s*[,)]/g)) names.add(m[1]);
|
|
8212
|
+
return names;
|
|
8213
|
+
}
|
|
8214
|
+
const DECLARED_TOKENS = declaredKaiTokens(themeCss);
|
|
8215
|
+
function colorToken(suffix) {
|
|
8216
|
+
const name = `--kai-color-${suffix}`;
|
|
8217
|
+
if (!DECLARED_TOKENS.has(name)) {
|
|
8218
|
+
throw new Error(
|
|
8219
|
+
`[kai mcp: theme] theme.css declares no \`${name}\`. The tool brands that token, so emitting it would hand the caller CSS that themes nothing. Either the token was renamed in packages/ui/theme.css (update the BRAND_TOKENS suffix in src/agent-tooling/mcp/tools/theme.ts to match) or it was removed (drop the entry). Known --kai-color-* names: ${[...DECLARED_TOKENS].filter((n) => n.startsWith("--kai-color-")).sort().join(", ")}`
|
|
8220
|
+
);
|
|
8221
|
+
}
|
|
8222
|
+
return name;
|
|
8223
|
+
}
|
|
8224
|
+
const BRAND_TOKENS = [
|
|
8225
|
+
{ key: "primary", suffix: "primary", purpose: "primary brand color" },
|
|
8226
|
+
{
|
|
8227
|
+
key: "primaryFg",
|
|
8228
|
+
suffix: "primary-foreground",
|
|
8229
|
+
purpose: "AA-contrast foreground on primary (auto-selected black/white)"
|
|
8230
|
+
},
|
|
8231
|
+
{ key: "ring", suffix: "ring", purpose: "keyboard focus ring" },
|
|
8232
|
+
{ key: "accent", suffix: "accent", purpose: "tinted accent surface" },
|
|
8233
|
+
{ key: "accentFg", suffix: "accent-foreground", purpose: "text on accent surface" }
|
|
8234
|
+
];
|
|
8235
|
+
let memo;
|
|
8236
|
+
function emittedTokens() {
|
|
8237
|
+
if (!memo) {
|
|
8238
|
+
const tokens = BRAND_TOKENS.map((t) => ({
|
|
8239
|
+
key: t.key,
|
|
8240
|
+
name: colorToken(t.suffix),
|
|
8241
|
+
purpose: t.purpose
|
|
8242
|
+
}));
|
|
8243
|
+
memo = { tokens, valueColumn: Math.max(...tokens.map((t) => t.name.length + 1)) + 2 };
|
|
8244
|
+
}
|
|
8245
|
+
return memo;
|
|
8246
|
+
}
|
|
6210
8247
|
function parseHex(hex) {
|
|
6211
8248
|
const s = hex.trim().replace(/^#/, "");
|
|
6212
8249
|
if (s.length === 3) {
|
|
6213
8250
|
const r = parseInt(s[0] + s[0], 16);
|
|
6214
|
-
const
|
|
8251
|
+
const g = parseInt(s[1] + s[1], 16);
|
|
6215
8252
|
const b = parseInt(s[2] + s[2], 16);
|
|
6216
|
-
if (isNaN(r) || isNaN(
|
|
6217
|
-
return { r, g
|
|
8253
|
+
if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
|
|
8254
|
+
return { r, g, b };
|
|
6218
8255
|
}
|
|
6219
8256
|
if (s.length === 6) {
|
|
6220
8257
|
const r = parseInt(s.slice(0, 2), 16);
|
|
6221
|
-
const
|
|
8258
|
+
const g = parseInt(s.slice(2, 4), 16);
|
|
6222
8259
|
const b = parseInt(s.slice(4, 6), 16);
|
|
6223
|
-
if (isNaN(r) || isNaN(
|
|
6224
|
-
return { r, g
|
|
8260
|
+
if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
|
|
8261
|
+
return { r, g, b };
|
|
6225
8262
|
}
|
|
6226
8263
|
return null;
|
|
6227
8264
|
}
|
|
6228
|
-
function toHex(r,
|
|
6229
|
-
return "#" + [r,
|
|
8265
|
+
function toHex(r, g, b) {
|
|
8266
|
+
return "#" + [r, g, b].map((v2) => Math.max(0, Math.min(255, Math.round(v2))).toString(16).padStart(2, "0")).join("");
|
|
6230
8267
|
}
|
|
6231
|
-
function relativeLuminance(r,
|
|
8268
|
+
function relativeLuminance(r, g, b) {
|
|
6232
8269
|
const lin = (c) => {
|
|
6233
8270
|
const s = c / 255;
|
|
6234
8271
|
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
6235
8272
|
};
|
|
6236
|
-
return 0.2126 * lin(r) + 0.7152 * lin(
|
|
8273
|
+
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
6237
8274
|
}
|
|
6238
|
-
function foreground(r,
|
|
6239
|
-
const lum = relativeLuminance(r,
|
|
8275
|
+
function foreground(r, g, b) {
|
|
8276
|
+
const lum = relativeLuminance(r, g, b);
|
|
6240
8277
|
const contrastWhite = 1.05 / (lum + 0.05);
|
|
6241
8278
|
const contrastBlack = (lum + 0.05) / 0.05;
|
|
6242
8279
|
return contrastWhite >= contrastBlack ? "#ffffff" : "#000000";
|
|
6243
8280
|
}
|
|
6244
|
-
function lighten(r,
|
|
8281
|
+
function lighten(r, g, b, ratio) {
|
|
6245
8282
|
return toHex(
|
|
6246
8283
|
r + (255 - r) * ratio,
|
|
6247
|
-
|
|
8284
|
+
g + (255 - g) * ratio,
|
|
6248
8285
|
b + (255 - b) * ratio
|
|
6249
8286
|
);
|
|
6250
8287
|
}
|
|
6251
|
-
function darken(r,
|
|
6252
|
-
return toHex(r * (1 - ratio),
|
|
8288
|
+
function darken(r, g, b, ratio) {
|
|
8289
|
+
return toHex(r * (1 - ratio), g * (1 - ratio), b * (1 - ratio));
|
|
6253
8290
|
}
|
|
6254
|
-
function accentSurface(r,
|
|
6255
|
-
return lighten(r,
|
|
8291
|
+
function accentSurface(r, g, b) {
|
|
8292
|
+
return lighten(r, g, b, 0.88);
|
|
6256
8293
|
}
|
|
6257
|
-
function darkModeBrand(r,
|
|
6258
|
-
const lum = relativeLuminance(r,
|
|
6259
|
-
if (lum > 0.25) return toHex(r,
|
|
6260
|
-
return lighten(r,
|
|
8294
|
+
function darkModeBrand(r, g, b) {
|
|
8295
|
+
const lum = relativeLuminance(r, g, b);
|
|
8296
|
+
if (lum > 0.25) return toHex(r, g, b);
|
|
8297
|
+
return lighten(r, g, b, 0.35);
|
|
6261
8298
|
}
|
|
6262
|
-
function darkAccentSurface(r,
|
|
6263
|
-
return darken(r,
|
|
8299
|
+
function darkAccentSurface(r, g, b) {
|
|
8300
|
+
return darken(r, g, b, 0.75);
|
|
6264
8301
|
}
|
|
6265
8302
|
const KEYWORD_COLORS = {
|
|
6266
8303
|
blue: "#3b82f6",
|
|
@@ -6328,10 +8365,10 @@ function resolveBrand(brand, description) {
|
|
|
6328
8365
|
note: `No brand color or description provided — defaulted to **${DEFAULT_BRAND}** (indigo, the kit's own brand). Pass \`brand: '#rrggbb'\` to use your color.`
|
|
6329
8366
|
};
|
|
6330
8367
|
}
|
|
6331
|
-
function buildTokenSet(r,
|
|
6332
|
-
const primary = toHex(r,
|
|
6333
|
-
const primaryFg = foreground(r,
|
|
6334
|
-
const accent = accentSurface(r,
|
|
8368
|
+
function buildTokenSet(r, g, b) {
|
|
8369
|
+
const primary = toHex(r, g, b);
|
|
8370
|
+
const primaryFg = foreground(r, g, b);
|
|
8371
|
+
const accent = accentSurface(r, g, b);
|
|
6335
8372
|
const accentFgColor = foreground(
|
|
6336
8373
|
...[accent].map((h) => parseHex(h)).flatMap((c) => [c.r, c.g, c.b])
|
|
6337
8374
|
);
|
|
@@ -6343,12 +8380,12 @@ function buildTokenSet(r, g2, b) {
|
|
|
6343
8380
|
accentFg: accentFgColor
|
|
6344
8381
|
};
|
|
6345
8382
|
}
|
|
6346
|
-
function buildDarkTokenSet(r,
|
|
6347
|
-
const darkBrand = darkModeBrand(r,
|
|
8383
|
+
function buildDarkTokenSet(r, g, b) {
|
|
8384
|
+
const darkBrand = darkModeBrand(r, g, b);
|
|
6348
8385
|
const parsed = parseHex(darkBrand);
|
|
6349
8386
|
const primary = darkBrand;
|
|
6350
8387
|
const primaryFg = foreground(parsed.r, parsed.g, parsed.b);
|
|
6351
|
-
const accent = darkAccentSurface(r,
|
|
8388
|
+
const accent = darkAccentSurface(r, g, b);
|
|
6352
8389
|
const accentFgParsed = parseHex(accent);
|
|
6353
8390
|
return {
|
|
6354
8391
|
primary,
|
|
@@ -6358,14 +8395,12 @@ function buildDarkTokenSet(r, g2, b) {
|
|
|
6358
8395
|
accentFg: foreground(accentFgParsed.r, accentFgParsed.g, accentFgParsed.b)
|
|
6359
8396
|
};
|
|
6360
8397
|
}
|
|
6361
|
-
function cssBlock(selector,
|
|
8398
|
+
function cssBlock(selector, values, emitted) {
|
|
6362
8399
|
return [
|
|
6363
8400
|
`${selector} {`,
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
` --kai-color-accent: ${tokens.accent};`,
|
|
6368
|
-
` --kai-color-accent-foreground: ${tokens.accentFg};`,
|
|
8401
|
+
...emitted.tokens.map(
|
|
8402
|
+
(t) => ` ${`${t.name}:`.padEnd(emitted.valueColumn)}${values[t.key]};`
|
|
8403
|
+
),
|
|
6369
8404
|
`}`
|
|
6370
8405
|
].join("\n");
|
|
6371
8406
|
}
|
|
@@ -6387,21 +8422,24 @@ const theme = {
|
|
|
6387
8422
|
description,
|
|
6388
8423
|
mode = "light"
|
|
6389
8424
|
} = args;
|
|
6390
|
-
const
|
|
6391
|
-
const
|
|
6392
|
-
const
|
|
8425
|
+
const emitted = emittedTokens();
|
|
8426
|
+
const { hex, r, g, b, note } = resolveBrand(brand, description);
|
|
8427
|
+
const lightTokens = buildTokenSet(r, g, b);
|
|
8428
|
+
const darkTokens = buildDarkTokenSet(r, g, b);
|
|
6393
8429
|
const blocks = [];
|
|
6394
8430
|
if (mode === "light" || mode === "both") {
|
|
6395
|
-
blocks.push(cssBlock(":root", lightTokens));
|
|
8431
|
+
blocks.push(cssBlock(":root", lightTokens, emitted));
|
|
6396
8432
|
}
|
|
6397
8433
|
if (mode === "dark" || mode === "both") {
|
|
6398
|
-
blocks.push(cssBlock(".dark", darkTokens));
|
|
8434
|
+
blocks.push(cssBlock(".dark", darkTokens, emitted));
|
|
6399
8435
|
}
|
|
6400
8436
|
const cssOutput = blocks.join("\n\n");
|
|
6401
8437
|
const noteLabel = note.startsWith("Could not parse") ? "Note" : "Assumption";
|
|
6402
8438
|
const noteSection = note ? `
|
|
6403
8439
|
> **${noteLabel}:** ${note}
|
|
6404
8440
|
` : "";
|
|
8441
|
+
const tokenList = emitted.tokens.map((t) => `- \`${t.name}\` — ${t.purpose}`).join("\n");
|
|
8442
|
+
const firstToken = emitted.tokens[0].name;
|
|
6405
8443
|
const text2 = `## AI/UI theme override — \`${hex}\`
|
|
6406
8444
|
${noteSection}
|
|
6407
8445
|
Paste this block into your **global stylesheet** (before the \`@import\` of \`@kitn.ai/ui\` or after — order doesn't matter since \`--kai-*\` tokens are resolved at runtime via \`var()\` fallbacks). For \`mode:'dark'\` overrides, the \`.dark\` class must be on a parent element (e.g. \`<html class="dark">\`) for them to take effect.
|
|
@@ -6412,14 +8450,10 @@ ${cssOutput}
|
|
|
6412
8450
|
|
|
6413
8451
|
### How it works
|
|
6414
8452
|
|
|
6415
|
-
The \`--kai-color-*\` tokens pierce the Shadow DOM via CSS custom-property inheritance. Every \`kai-*\` element reads them through a \`var(
|
|
8453
|
+
The \`--kai-color-*\` tokens pierce the Shadow DOM via CSS custom-property inheritance. Every \`kai-*\` element reads them through a \`var(${firstToken}, <default>)\` fallback chain defined in \`theme.css\`. Setting them on \`:root\` (or a wrapper element) is enough to rebrand everything globally.
|
|
6416
8454
|
|
|
6417
|
-
**Tokens emitted** (
|
|
6418
|
-
|
|
6419
|
-
- \`--kai-color-primary-foreground\` — AA-contrast foreground on primary (auto-selected black/white)
|
|
6420
|
-
- \`--kai-color-ring\` — keyboard focus ring
|
|
6421
|
-
- \`--kai-color-accent\` — tinted accent surface
|
|
6422
|
-
- \`--kai-color-accent-foreground\` — text on accent surface
|
|
8455
|
+
**Tokens emitted** (names read from \`theme.css\`):
|
|
8456
|
+
${tokenList}
|
|
6423
8457
|
|
|
6424
8458
|
### Apply to a subtree only
|
|
6425
8459
|
|
|
@@ -6620,12 +8654,96 @@ const debug = {
|
|
|
6620
8654
|
};
|
|
6621
8655
|
}
|
|
6622
8656
|
};
|
|
8657
|
+
function editDistance(a, b) {
|
|
8658
|
+
const prev = new Array(b.length + 1);
|
|
8659
|
+
for (let j2 = 0; j2 <= b.length; j2++) prev[j2] = j2;
|
|
8660
|
+
for (let i = 1; i <= a.length; i++) {
|
|
8661
|
+
let diag = prev[0];
|
|
8662
|
+
prev[0] = i;
|
|
8663
|
+
for (let j2 = 1; j2 <= b.length; j2++) {
|
|
8664
|
+
const tmp = prev[j2];
|
|
8665
|
+
prev[j2] = Math.min(
|
|
8666
|
+
prev[j2] + 1,
|
|
8667
|
+
prev[j2 - 1] + 1,
|
|
8668
|
+
diag + (a[i - 1] === b[j2 - 1] ? 0 : 1)
|
|
8669
|
+
);
|
|
8670
|
+
diag = tmp;
|
|
8671
|
+
}
|
|
8672
|
+
}
|
|
8673
|
+
return prev[b.length];
|
|
8674
|
+
}
|
|
8675
|
+
function typeWord(prop) {
|
|
8676
|
+
if (!prop) return "value";
|
|
8677
|
+
if (Array.isArray(prop.enum)) return prop.enum.map((v2) => JSON.stringify(v2)).join(" | ");
|
|
8678
|
+
if (prop.type === "array") {
|
|
8679
|
+
const items = prop.items;
|
|
8680
|
+
return `array of ${typeof items?.type === "string" ? items.type : "values"}`;
|
|
8681
|
+
}
|
|
8682
|
+
return typeof prop.type === "string" ? prop.type : "value";
|
|
8683
|
+
}
|
|
8684
|
+
function suggestKey(unknownKey, value, shape, supplied) {
|
|
8685
|
+
const keys = Object.keys(shape);
|
|
8686
|
+
let best;
|
|
8687
|
+
let bestDistance = 3;
|
|
8688
|
+
for (const key of keys) {
|
|
8689
|
+
const d = editDistance(unknownKey.toLowerCase(), key.toLowerCase());
|
|
8690
|
+
if (d < bestDistance) {
|
|
8691
|
+
bestDistance = d;
|
|
8692
|
+
best = key;
|
|
8693
|
+
}
|
|
8694
|
+
}
|
|
8695
|
+
if (best !== void 0) return best;
|
|
8696
|
+
const byValue = keys.filter(
|
|
8697
|
+
(key) => !supplied.includes(key) && z$1.safeParse(shape[key], value).success
|
|
8698
|
+
);
|
|
8699
|
+
return byValue.length === 1 ? byValue[0] : void 0;
|
|
8700
|
+
}
|
|
8701
|
+
function validateToolArgs(toolName, schema, args) {
|
|
8702
|
+
const shape = schema.shape;
|
|
8703
|
+
const keys = Object.keys(shape);
|
|
8704
|
+
const supplied = Object.keys(args);
|
|
8705
|
+
const problems = [];
|
|
8706
|
+
for (const key of supplied) {
|
|
8707
|
+
if (keys.includes(key)) continue;
|
|
8708
|
+
const suggestion = suggestKey(key, args[key], shape, supplied);
|
|
8709
|
+
problems.push(
|
|
8710
|
+
`unknown argument ${JSON.stringify(key)}` + (suggestion ? ` — did you mean ${JSON.stringify(suggestion)}?` : "")
|
|
8711
|
+
);
|
|
8712
|
+
}
|
|
8713
|
+
for (const key of keys) {
|
|
8714
|
+
if (!(key in args) && !z$1.safeParse(shape[key], void 0).success) {
|
|
8715
|
+
problems.push(`missing required argument ${JSON.stringify(key)}`);
|
|
8716
|
+
}
|
|
8717
|
+
}
|
|
8718
|
+
if (problems.length === 0) return void 0;
|
|
8719
|
+
const json = z$1.toJSONSchema(schema);
|
|
8720
|
+
const required = new Set(json.required ?? []);
|
|
8721
|
+
const argLines = keys.map((key) => {
|
|
8722
|
+
const kind = required.has(key) ? "required" : "optional";
|
|
8723
|
+
return ` ${key} (${kind}) — ${typeWord(json.properties?.[key])}`;
|
|
8724
|
+
});
|
|
8725
|
+
return `${toolName}: ${problems.join("; ")}.
|
|
8726
|
+
|
|
8727
|
+
Nothing was returned for this call — answering anyway could look like a reply to the question you meant to ask.
|
|
8728
|
+
|
|
8729
|
+
Expected arguments for ${toolName}:
|
|
8730
|
+
${argLines.join("\n")}`;
|
|
8731
|
+
}
|
|
6623
8732
|
const tools = [reference, scaffold, theme, debug];
|
|
8733
|
+
const PACKAGE_NAME = "@kitn.ai/ui";
|
|
8734
|
+
function packageIdentity() {
|
|
8735
|
+
const specifier = `${PACKAGE_NAME}/package.json`;
|
|
8736
|
+
const require2 = createRequire(import.meta.url);
|
|
8737
|
+
const pkg = require2(specifier);
|
|
8738
|
+
if (pkg.name !== PACKAGE_NAME || typeof pkg.version !== "string") {
|
|
8739
|
+
throw new Error(
|
|
8740
|
+
`[${PACKAGE_NAME}] Refusing to report a serverInfo that is not this package's: ${specifier} resolved to ${require2.resolve(specifier)}, whose name is ${JSON.stringify(pkg.name)} and version ${JSON.stringify(pkg.version)}.`
|
|
8741
|
+
);
|
|
8742
|
+
}
|
|
8743
|
+
return { name: pkg.name, version: pkg.version };
|
|
8744
|
+
}
|
|
6624
8745
|
function createServer() {
|
|
6625
|
-
const server = new Server(
|
|
6626
|
-
{ name: "@kitn.ai/ui", version: "0.15.0" },
|
|
6627
|
-
{ capabilities: { tools: {} } }
|
|
6628
|
-
);
|
|
8746
|
+
const server = new Server(packageIdentity(), { capabilities: { tools: {} } });
|
|
6629
8747
|
const byName = new Map(tools.map((t) => [t.name, t]));
|
|
6630
8748
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
6631
8749
|
tools: tools.map((t) => ({
|
|
@@ -6645,7 +8763,12 @@ function createServer() {
|
|
|
6645
8763
|
content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }]
|
|
6646
8764
|
};
|
|
6647
8765
|
}
|
|
6648
|
-
|
|
8766
|
+
const args = request.params.arguments ?? {};
|
|
8767
|
+
const problem = validateToolArgs(tool.name, tool.inputSchema, args);
|
|
8768
|
+
if (problem) {
|
|
8769
|
+
return { isError: true, content: [{ type: "text", text: problem }] };
|
|
8770
|
+
}
|
|
8771
|
+
return tool.handler(args);
|
|
6649
8772
|
});
|
|
6650
8773
|
server.__listToolsForTest = () => tools.map((t) => t.name);
|
|
6651
8774
|
return server;
|