@tai42/studio-sdk 0.2.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/LICENSE +202 -0
- package/NOTICE +15 -0
- package/README.md +318 -0
- package/SECURITY.md +51 -0
- package/dist/components/badge.d.ts +13 -0
- package/dist/components/badge.d.ts.map +1 -0
- package/dist/components/badge.js +14 -0
- package/dist/components/badge.js.map +1 -0
- package/dist/components/checkbox.d.ts +24 -0
- package/dist/components/checkbox.d.ts.map +1 -0
- package/dist/components/checkbox.js +31 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/code-block.d.ts +7 -0
- package/dist/components/code-block.d.ts.map +1 -0
- package/dist/components/code-block.js +23 -0
- package/dist/components/code-block.js.map +1 -0
- package/dist/components/completion-input.d.ts +31 -0
- package/dist/components/completion-input.d.ts.map +1 -0
- package/dist/components/completion-input.js +61 -0
- package/dist/components/completion-input.js.map +1 -0
- package/dist/components/components.css +2212 -0
- package/dist/components/confirm-dialog.d.ts +40 -0
- package/dist/components/confirm-dialog.d.ts.map +1 -0
- package/dist/components/confirm-dialog.js +11 -0
- package/dist/components/confirm-dialog.js.map +1 -0
- package/dist/components/control-styles.d.ts +12 -0
- package/dist/components/control-styles.d.ts.map +1 -0
- package/dist/components/control-styles.js +14 -0
- package/dist/components/control-styles.js.map +1 -0
- package/dist/components/copy-field.d.ts +8 -0
- package/dist/components/copy-field.d.ts.map +1 -0
- package/dist/components/copy-field.js +106 -0
- package/dist/components/copy-field.js.map +1 -0
- package/dist/components/dialog.d.ts +21 -0
- package/dist/components/dialog.d.ts.map +1 -0
- package/dist/components/dialog.js +25 -0
- package/dist/components/dialog.js.map +1 -0
- package/dist/components/drawer.d.ts +22 -0
- package/dist/components/drawer.d.ts.map +1 -0
- package/dist/components/drawer.js +32 -0
- package/dist/components/drawer.js.map +1 -0
- package/dist/components/error-boundary.d.ts +33 -0
- package/dist/components/error-boundary.d.ts.map +1 -0
- package/dist/components/error-boundary.js +36 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/extension-combo-builder.d.ts +70 -0
- package/dist/components/extension-combo-builder.d.ts.map +1 -0
- package/dist/components/extension-combo-builder.js +154 -0
- package/dist/components/extension-combo-builder.js.map +1 -0
- package/dist/components/extension-grouping.d.ts +65 -0
- package/dist/components/extension-grouping.d.ts.map +1 -0
- package/dist/components/extension-grouping.js +78 -0
- package/dist/components/extension-grouping.js.map +1 -0
- package/dist/components/extension-picker.d.ts +35 -0
- package/dist/components/extension-picker.d.ts.map +1 -0
- package/dist/components/extension-picker.js +31 -0
- package/dist/components/extension-picker.js.map +1 -0
- package/dist/components/external-link.d.ts +19 -0
- package/dist/components/external-link.d.ts.map +1 -0
- package/dist/components/external-link.js +12 -0
- package/dist/components/external-link.js.map +1 -0
- package/dist/components/field.d.ts +64 -0
- package/dist/components/field.d.ts.map +1 -0
- package/dist/components/field.js +56 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/fleet-report.d.ts +39 -0
- package/dist/components/fleet-report.d.ts.map +1 -0
- package/dist/components/fleet-report.js +50 -0
- package/dist/components/fleet-report.js.map +1 -0
- package/dist/components/fonts.css +22 -0
- package/dist/components/icons.d.ts +142 -0
- package/dist/components/icons.d.ts.map +1 -0
- package/dist/components/icons.js +139 -0
- package/dist/components/icons.js.map +1 -0
- package/dist/components/inputs.d.ts +52 -0
- package/dist/components/inputs.d.ts.map +1 -0
- package/dist/components/inputs.js +35 -0
- package/dist/components/inputs.js.map +1 -0
- package/dist/components/json-diff.d.ts +40 -0
- package/dist/components/json-diff.d.ts.map +1 -0
- package/dist/components/json-diff.js +119 -0
- package/dist/components/json-diff.js.map +1 -0
- package/dist/components/json-tree.d.ts +8 -0
- package/dist/components/json-tree.d.ts.map +1 -0
- package/dist/components/json-tree.js +70 -0
- package/dist/components/json-tree.js.map +1 -0
- package/dist/components/modal-focus.d.ts +15 -0
- package/dist/components/modal-focus.d.ts.map +1 -0
- package/dist/components/modal-focus.js +136 -0
- package/dist/components/modal-focus.js.map +1 -0
- package/dist/components/page-header.d.ts +56 -0
- package/dist/components/page-header.d.ts.map +1 -0
- package/dist/components/page-header.js +24 -0
- package/dist/components/page-header.js.map +1 -0
- package/dist/components/primitives.d.ts +186 -0
- package/dist/components/primitives.d.ts.map +1 -0
- package/dist/components/primitives.js +204 -0
- package/dist/components/primitives.js.map +1 -0
- package/dist/components/progress-bar.d.ts +29 -0
- package/dist/components/progress-bar.d.ts.map +1 -0
- package/dist/components/progress-bar.js +48 -0
- package/dist/components/progress-bar.js.map +1 -0
- package/dist/components/radio-group.d.ts +37 -0
- package/dist/components/radio-group.d.ts.map +1 -0
- package/dist/components/radio-group.js +56 -0
- package/dist/components/radio-group.js.map +1 -0
- package/dist/components/reveal-input.d.ts +13 -0
- package/dist/components/reveal-input.d.ts.map +1 -0
- package/dist/components/reveal-input.js +31 -0
- package/dist/components/reveal-input.js.map +1 -0
- package/dist/components/scroll-region.d.ts +85 -0
- package/dist/components/scroll-region.d.ts.map +1 -0
- package/dist/components/scroll-region.js +574 -0
- package/dist/components/scroll-region.js.map +1 -0
- package/dist/components/select.d.ts +53 -0
- package/dist/components/select.d.ts.map +1 -0
- package/dist/components/select.js +33 -0
- package/dist/components/select.js.map +1 -0
- package/dist/components/table.d.ts +59 -0
- package/dist/components/table.d.ts.map +1 -0
- package/dist/components/table.js +20 -0
- package/dist/components/table.js.map +1 -0
- package/dist/components/tabs.d.ts +15 -0
- package/dist/components/tabs.d.ts.map +1 -0
- package/dist/components/tabs.js +13 -0
- package/dist/components/tabs.js.map +1 -0
- package/dist/components/tags.d.ts +46 -0
- package/dist/components/tags.d.ts.map +1 -0
- package/dist/components/tags.js +68 -0
- package/dist/components/tags.js.map +1 -0
- package/dist/components/tokens.css +392 -0
- package/dist/components/tokens.d.ts +15 -0
- package/dist/components/tokens.d.ts.map +1 -0
- package/dist/components/tokens.js +101 -0
- package/dist/components/tokens.js.map +1 -0
- package/dist/components/tool-picker.d.ts +24 -0
- package/dist/components/tool-picker.d.ts.map +1 -0
- package/dist/components/tool-picker.js +97 -0
- package/dist/components/tool-picker.js.map +1 -0
- package/dist/components/tooltip.d.ts +17 -0
- package/dist/components/tooltip.d.ts.map +1 -0
- package/dist/components/tooltip.js +18 -0
- package/dist/components/tooltip.js.map +1 -0
- package/dist/components/version-history-panel.d.ts +71 -0
- package/dist/components/version-history-panel.d.ts.map +1 -0
- package/dist/components/version-history-panel.js +148 -0
- package/dist/components/version-history-panel.js.map +1 -0
- package/dist/download.d.ts +7 -0
- package/dist/download.d.ts.map +1 -0
- package/dist/download.js +18 -0
- package/dist/download.js.map +1 -0
- package/dist/element-slot.d.ts +4 -0
- package/dist/element-slot.d.ts.map +1 -0
- package/dist/element-slot.js +20 -0
- package/dist/element-slot.js.map +1 -0
- package/dist/elicitation/ElicitationForm.d.ts +34 -0
- package/dist/elicitation/ElicitationForm.d.ts.map +1 -0
- package/dist/elicitation/ElicitationForm.js +24 -0
- package/dist/elicitation/ElicitationForm.js.map +1 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +15 -0
- package/dist/errors.js.map +1 -0
- package/dist/extension-combos.d.ts +13 -0
- package/dist/extension-combos.d.ts.map +1 -0
- package/dist/extension-combos.js +9 -0
- package/dist/extension-combos.js.map +1 -0
- package/dist/hooks/useApi.d.ts +1222 -0
- package/dist/hooks/useApi.d.ts.map +1 -0
- package/dist/hooks/useApi.js +22 -0
- package/dist/hooks/useApi.js.map +1 -0
- package/dist/hooks/useAuth.d.ts +13 -0
- package/dist/hooks/useAuth.d.ts.map +1 -0
- package/dist/hooks/useAuth.js +53 -0
- package/dist/hooks/useAuth.js.map +1 -0
- package/dist/hooks/useBreakpoint.d.ts +10 -0
- package/dist/hooks/useBreakpoint.d.ts.map +1 -0
- package/dist/hooks/useBreakpoint.js +66 -0
- package/dist/hooks/useBreakpoint.js.map +1 -0
- package/dist/hooks/useCapabilities.d.ts +88 -0
- package/dist/hooks/useCapabilities.d.ts.map +1 -0
- package/dist/hooks/useCapabilities.js +203 -0
- package/dist/hooks/useCapabilities.js.map +1 -0
- package/dist/hooks/useSse.d.ts +13 -0
- package/dist/hooks/useSse.d.ts.map +1 -0
- package/dist/hooks/useSse.js +298 -0
- package/dist/hooks/useSse.js.map +1 -0
- package/dist/hooks/useTheme.d.ts +20 -0
- package/dist/hooks/useTheme.d.ts.map +1 -0
- package/dist/hooks/useTheme.js +97 -0
- package/dist/hooks/useTheme.js.map +1 -0
- package/dist/hooks/useUnauthorized.d.ts +3 -0
- package/dist/hooks/useUnauthorized.d.ts.map +1 -0
- package/dist/hooks/useUnauthorized.js +18 -0
- package/dist/hooks/useUnauthorized.js.map +1 -0
- package/dist/host.d.ts +11 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +10 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation/context.d.ts +25 -0
- package/dist/navigation/context.d.ts.map +1 -0
- package/dist/navigation/context.js +45 -0
- package/dist/navigation/context.js.map +1 -0
- package/dist/navigation/index.d.ts +4 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +2 -0
- package/dist/navigation/index.js.map +1 -0
- package/dist/navigation/types.d.ts +97 -0
- package/dist/navigation/types.d.ts.map +1 -0
- package/dist/navigation/types.js +16 -0
- package/dist/navigation/types.js.map +1 -0
- package/dist/plugin/host-state.d.ts +45 -0
- package/dist/plugin/host-state.d.ts.map +1 -0
- package/dist/plugin/host-state.js +62 -0
- package/dist/plugin/host-state.js.map +1 -0
- package/dist/plugin/registry.d.ts +35 -0
- package/dist/plugin/registry.d.ts.map +1 -0
- package/dist/plugin/registry.js +129 -0
- package/dist/plugin/registry.js.map +1 -0
- package/dist/plugin/types.d.ts +165 -0
- package/dist/plugin/types.d.ts.map +1 -0
- package/dist/plugin/types.js +2 -0
- package/dist/plugin/types.js.map +1 -0
- package/dist/plugin/version.d.ts +34 -0
- package/dist/plugin/version.d.ts.map +1 -0
- package/dist/plugin/version.js +37 -0
- package/dist/plugin/version.js.map +1 -0
- package/dist/query-keys.d.ts +31 -0
- package/dist/query-keys.d.ts.map +1 -0
- package/dist/query-keys.js +31 -0
- package/dist/query-keys.js.map +1 -0
- package/dist/schema-editor/SchemaEditor.d.ts +43 -0
- package/dist/schema-editor/SchemaEditor.d.ts.map +1 -0
- package/dist/schema-editor/SchemaEditor.js +106 -0
- package/dist/schema-editor/SchemaEditor.js.map +1 -0
- package/dist/schema-editor/index.d.ts +10 -0
- package/dist/schema-editor/index.d.ts.map +1 -0
- package/dist/schema-editor/index.js +8 -0
- package/dist/schema-editor/index.js.map +1 -0
- package/dist/schema-editor/lint.d.ts +37 -0
- package/dist/schema-editor/lint.d.ts.map +1 -0
- package/dist/schema-editor/lint.js +49 -0
- package/dist/schema-editor/lint.js.map +1 -0
- package/dist/schema-form/SchemaForm.d.ts +70 -0
- package/dist/schema-form/SchemaForm.d.ts.map +1 -0
- package/dist/schema-form/SchemaForm.js +13 -0
- package/dist/schema-form/SchemaForm.js.map +1 -0
- package/dist/schema-form/array-field.d.ts +20 -0
- package/dist/schema-form/array-field.d.ts.map +1 -0
- package/dist/schema-form/array-field.js +39 -0
- package/dist/schema-form/array-field.js.map +1 -0
- package/dist/schema-form/classify.d.ts +87 -0
- package/dist/schema-form/classify.d.ts.map +1 -0
- package/dist/schema-form/classify.js +199 -0
- package/dist/schema-form/classify.js.map +1 -0
- package/dist/schema-form/context.d.ts +4 -0
- package/dist/schema-form/context.d.ts.map +1 -0
- package/dist/schema-form/context.js +14 -0
- package/dist/schema-form/context.js.map +1 -0
- package/dist/schema-form/default-value.d.ts +8 -0
- package/dist/schema-form/default-value.d.ts.map +1 -0
- package/dist/schema-form/default-value.js +64 -0
- package/dist/schema-form/default-value.js.map +1 -0
- package/dist/schema-form/enum-field.d.ts +19 -0
- package/dist/schema-form/enum-field.d.ts.map +1 -0
- package/dist/schema-form/enum-field.js +26 -0
- package/dist/schema-form/enum-field.js.map +1 -0
- package/dist/schema-form/field-group.d.ts +9 -0
- package/dist/schema-form/field-group.d.ts.map +1 -0
- package/dist/schema-form/field-group.js +32 -0
- package/dist/schema-form/field-group.js.map +1 -0
- package/dist/schema-form/field-node.d.ts +24 -0
- package/dist/schema-form/field-node.d.ts.map +1 -0
- package/dist/schema-form/field-node.js +51 -0
- package/dist/schema-form/field-node.js.map +1 -0
- package/dist/schema-form/index.d.ts +13 -0
- package/dist/schema-form/index.d.ts.map +1 -0
- package/dist/schema-form/index.js +11 -0
- package/dist/schema-form/index.js.map +1 -0
- package/dist/schema-form/media-field.d.ts +24 -0
- package/dist/schema-form/media-field.d.ts.map +1 -0
- package/dist/schema-form/media-field.js +169 -0
- package/dist/schema-form/media-field.js.map +1 -0
- package/dist/schema-form/media.d.ts +40 -0
- package/dist/schema-form/media.d.ts.map +1 -0
- package/dist/schema-form/media.js +65 -0
- package/dist/schema-form/media.js.map +1 -0
- package/dist/schema-form/object-fields.d.ts +20 -0
- package/dist/schema-form/object-fields.d.ts.map +1 -0
- package/dist/schema-form/object-fields.js +24 -0
- package/dist/schema-form/object-fields.js.map +1 -0
- package/dist/schema-form/resolve.d.ts +41 -0
- package/dist/schema-form/resolve.d.ts.map +1 -0
- package/dist/schema-form/resolve.js +102 -0
- package/dist/schema-form/resolve.js.map +1 -0
- package/dist/schema-form/string-field.d.ts +20 -0
- package/dist/schema-form/string-field.d.ts.map +1 -0
- package/dist/schema-form/string-field.js +68 -0
- package/dist/schema-form/string-field.js.map +1 -0
- package/dist/schema-form/styles.d.ts +18 -0
- package/dist/schema-form/styles.d.ts.map +1 -0
- package/dist/schema-form/styles.js +18 -0
- package/dist/schema-form/styles.js.map +1 -0
- package/dist/schema-form/types.d.ts +67 -0
- package/dist/schema-form/types.d.ts.map +1 -0
- package/dist/schema-form/types.js +10 -0
- package/dist/schema-form/types.js.map +1 -0
- package/dist/schema-form/union-field.d.ts +24 -0
- package/dist/schema-form/union-field.d.ts.map +1 -0
- package/dist/schema-form/union-field.js +55 -0
- package/dist/schema-form/union-field.js.map +1 -0
- package/dist/schema-form/union.d.ts +4 -0
- package/dist/schema-form/union.d.ts.map +1 -0
- package/dist/schema-form/union.js +68 -0
- package/dist/schema-form/union.js.map +1 -0
- package/dist/schema-form/unsupported-notice.d.ts +18 -0
- package/dist/schema-form/unsupported-notice.d.ts.map +1 -0
- package/dist/schema-form/unsupported-notice.js +8 -0
- package/dist/schema-form/unsupported-notice.js.map +1 -0
- package/dist/schema-form/validate.d.ts +18 -0
- package/dist/schema-form/validate.d.ts.map +1 -0
- package/dist/schema-form/validate.js +162 -0
- package/dist/schema-form/validate.js.map +1 -0
- package/dist/structured-output/StructuredOutput.d.ts +27 -0
- package/dist/structured-output/StructuredOutput.d.ts.map +1 -0
- package/dist/structured-output/StructuredOutput.js +41 -0
- package/dist/structured-output/StructuredOutput.js.map +1 -0
- package/dist/testing.d.ts +80 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +207 -0
- package/dist/testing.js.map +1 -0
- package/package.json +101 -0
- package/src/components/badge.tsx +31 -0
- package/src/components/checkbox.tsx +93 -0
- package/src/components/code-block.tsx +37 -0
- package/src/components/completion-input.tsx +137 -0
- package/src/components/components.css +2212 -0
- package/src/components/confirm-dialog.tsx +77 -0
- package/src/components/control-styles.ts +14 -0
- package/src/components/copy-field.tsx +178 -0
- package/src/components/dialog.tsx +76 -0
- package/src/components/drawer.tsx +83 -0
- package/src/components/error-boundary.tsx +56 -0
- package/src/components/extension-combo-builder.tsx +305 -0
- package/src/components/extension-grouping.ts +116 -0
- package/src/components/extension-picker.tsx +97 -0
- package/src/components/external-link.tsx +40 -0
- package/src/components/field.tsx +169 -0
- package/src/components/fleet-report.tsx +136 -0
- package/src/components/fonts.css +22 -0
- package/src/components/icons.tsx +504 -0
- package/src/components/inputs.tsx +125 -0
- package/src/components/json-diff.tsx +192 -0
- package/src/components/json-tree.tsx +129 -0
- package/src/components/modal-focus.ts +150 -0
- package/src/components/page-header.tsx +107 -0
- package/src/components/primitives.tsx +489 -0
- package/src/components/progress-bar.tsx +100 -0
- package/src/components/radio-group.tsx +160 -0
- package/src/components/reveal-input.tsx +87 -0
- package/src/components/scroll-region.tsx +703 -0
- package/src/components/select.tsx +149 -0
- package/src/components/table.tsx +94 -0
- package/src/components/tabs.tsx +51 -0
- package/src/components/tags.tsx +137 -0
- package/src/components/tokens.css +392 -0
- package/src/components/tokens.ts +102 -0
- package/src/components/tool-picker.tsx +175 -0
- package/src/components/tooltip.tsx +46 -0
- package/src/components/version-history-panel.tsx +407 -0
- package/src/css.d.ts +2 -0
- package/src/download.ts +17 -0
- package/src/element-slot.ts +22 -0
- package/src/elicitation/ElicitationForm.tsx +87 -0
- package/src/errors.ts +12 -0
- package/src/extension-combos.ts +18 -0
- package/src/hooks/useApi.ts +27 -0
- package/src/hooks/useAuth.ts +67 -0
- package/src/hooks/useBreakpoint.ts +78 -0
- package/src/hooks/useCapabilities.ts +242 -0
- package/src/hooks/useSse.ts +314 -0
- package/src/hooks/useTheme.ts +133 -0
- package/src/hooks/useUnauthorized.ts +20 -0
- package/src/host.ts +15 -0
- package/src/index.ts +267 -0
- package/src/navigation/context.tsx +90 -0
- package/src/navigation/index.ts +9 -0
- package/src/navigation/types.ts +90 -0
- package/src/plugin/host-state.ts +93 -0
- package/src/plugin/registry.ts +174 -0
- package/src/plugin/types.ts +178 -0
- package/src/plugin/version.ts +44 -0
- package/src/query-keys.ts +34 -0
- package/src/schema-editor/SchemaEditor.tsx +186 -0
- package/src/schema-editor/index.ts +9 -0
- package/src/schema-editor/lint.ts +70 -0
- package/src/schema-form/SchemaForm.tsx +107 -0
- package/src/schema-form/array-field.tsx +110 -0
- package/src/schema-form/classify.ts +302 -0
- package/src/schema-form/context.ts +17 -0
- package/src/schema-form/default-value.ts +65 -0
- package/src/schema-form/enum-field.tsx +65 -0
- package/src/schema-form/field-group.tsx +71 -0
- package/src/schema-form/field-node.tsx +161 -0
- package/src/schema-form/index.ts +12 -0
- package/src/schema-form/media-field.tsx +281 -0
- package/src/schema-form/media.ts +69 -0
- package/src/schema-form/object-fields.tsx +74 -0
- package/src/schema-form/resolve.ts +114 -0
- package/src/schema-form/string-field.tsx +161 -0
- package/src/schema-form/styles.ts +20 -0
- package/src/schema-form/types.ts +71 -0
- package/src/schema-form/union-field.tsx +122 -0
- package/src/schema-form/union.ts +78 -0
- package/src/schema-form/unsupported-notice.tsx +39 -0
- package/src/schema-form/validate.ts +183 -0
- package/src/structured-output/StructuredOutput.tsx +81 -0
- package/src/testing.ts +235 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The in-memory registry the shell reads, plus {@link loadPlugin} — the one entry
|
|
3
|
+
* through which a plugin bundle's `register(context)` contributes to it. This module
|
|
4
|
+
* is bundled only into the served `@tai42/studio-sdk/host` singleton (external across
|
|
5
|
+
* the plugin boundary, like react and react-dom), so the module-level registry is
|
|
6
|
+
* the SAME instance every host caller — the shell and the feature panels — sees. The
|
|
7
|
+
* plugin surface (`@tai42/studio-sdk`) does not include it, so a plugin cannot reach it.
|
|
8
|
+
*/
|
|
9
|
+
import type {
|
|
10
|
+
NavEntryContribution,
|
|
11
|
+
PageContribution,
|
|
12
|
+
PluginContext,
|
|
13
|
+
PluginContributions,
|
|
14
|
+
PluginEntry,
|
|
15
|
+
RegisteredNavEntry,
|
|
16
|
+
RegisteredPage,
|
|
17
|
+
RegisteredSettingsTab,
|
|
18
|
+
SettingsTabContribution,
|
|
19
|
+
ToolPanelContribution,
|
|
20
|
+
} from './types';
|
|
21
|
+
|
|
22
|
+
const toolPanels = new Map<string, ToolPanelContribution>();
|
|
23
|
+
const pages: RegisteredPage[] = [];
|
|
24
|
+
const settingsTabs: RegisteredSettingsTab[] = [];
|
|
25
|
+
const navEntries: RegisteredNavEntry[] = [];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load one plugin: call its `register` entry with a context bound to `pluginId`,
|
|
29
|
+
* then commit everything it staged into the global registry.
|
|
30
|
+
*
|
|
31
|
+
* Contributions are STAGED into local arrays (identity captured in the closure,
|
|
32
|
+
* never read from ambient state) and only committed after `entry` settles
|
|
33
|
+
* successfully. `entry` is AWAITED before commit, so a synchronous or an `async`
|
|
34
|
+
* entry is fully done first: a post-`await` throw skips the commit (atomicity
|
|
35
|
+
* holds for async too) and a post-`await` registration cannot slip past the batch.
|
|
36
|
+
* The seal is set after `entry` has SETTLED — for a sync entry, after it returns
|
|
37
|
+
* and the `await` continuation runs; for an async entry, after its returned promise
|
|
38
|
+
* resolves. A registration attempted from that point on (a deferred timer, a
|
|
39
|
+
* callback scheduled after settle) throws loudly instead of pushing into an
|
|
40
|
+
* orphaned staged array that never commits. Registrations made during `entry`'s
|
|
41
|
+
* synchronous body — including a microtask it enqueues then, which runs before the
|
|
42
|
+
* `await` continuation — are still captured and committed with the batch. A
|
|
43
|
+
* `register` that throws commits nothing and leaves the registry untouched.
|
|
44
|
+
* Duplicate guards run against BOTH the staged contributions and what is already
|
|
45
|
+
* committed, and fail loudly.
|
|
46
|
+
*/
|
|
47
|
+
export async function loadPlugin(pluginId: string, entry: PluginEntry): Promise<void> {
|
|
48
|
+
const stagedPages: RegisteredPage[] = [];
|
|
49
|
+
const stagedToolPanels = new Map<string, ToolPanelContribution>();
|
|
50
|
+
const stagedSettingsTabs: RegisteredSettingsTab[] = [];
|
|
51
|
+
const stagedTabIds = new Set<string>();
|
|
52
|
+
const stagedNavEntries: RegisteredNavEntry[] = [];
|
|
53
|
+
let sealed = false;
|
|
54
|
+
|
|
55
|
+
const context: PluginContext = {
|
|
56
|
+
registerPage(contribution: PageContribution): void {
|
|
57
|
+
if (sealed) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
'registration is closed: a plugin must register during register(), not after it resolves',
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const staged = stagedPages.some((p) => p.path === contribution.path);
|
|
63
|
+
const committed = pages.some((p) => p.pluginId === pluginId && p.path === contribution.path);
|
|
64
|
+
if (staged || committed) {
|
|
65
|
+
// A duplicate path within one plugin is an author mistake that would
|
|
66
|
+
// leave one page unreachable — fail loudly, as registerToolPanel does.
|
|
67
|
+
throw new Error(`plugin “${pluginId}” already registered a page at “${contribution.path}”`);
|
|
68
|
+
}
|
|
69
|
+
stagedPages.push({ ...contribution, pluginId });
|
|
70
|
+
},
|
|
71
|
+
registerToolPanel(contribution: ToolPanelContribution): void {
|
|
72
|
+
if (sealed) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
'registration is closed: a plugin must register during register(), not after it resolves',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (stagedToolPanels.has(contribution.toolName) || toolPanels.has(contribution.toolName)) {
|
|
78
|
+
throw new Error(`a tool panel is already registered for ${contribution.toolName}`);
|
|
79
|
+
}
|
|
80
|
+
stagedToolPanels.set(contribution.toolName, contribution);
|
|
81
|
+
},
|
|
82
|
+
registerSettingsTab(contribution: SettingsTabContribution): void {
|
|
83
|
+
if (sealed) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
'registration is closed: a plugin must register during register(), not after it resolves',
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const committed = settingsTabs.some(
|
|
89
|
+
(t) => t.pluginId === pluginId && t.id === contribution.id,
|
|
90
|
+
);
|
|
91
|
+
if (stagedTabIds.has(contribution.id) || committed) {
|
|
92
|
+
// A duplicate tab id within one plugin would leave one tab unreachable —
|
|
93
|
+
// fail loudly, mirroring the page-path guard.
|
|
94
|
+
throw new Error(
|
|
95
|
+
`plugin “${pluginId}” already registered a settings tab “${contribution.id}”`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
stagedTabIds.add(contribution.id);
|
|
99
|
+
stagedSettingsTabs.push({ ...contribution, pluginId });
|
|
100
|
+
},
|
|
101
|
+
registerNavEntry(contribution: NavEntryContribution): void {
|
|
102
|
+
if (sealed) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
'registration is closed: a plugin must register during register(), not after it resolves',
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const staged = stagedNavEntries.some((e) => e.path === contribution.path);
|
|
108
|
+
const committed = navEntries.some(
|
|
109
|
+
(e) => e.pluginId === pluginId && e.path === contribution.path,
|
|
110
|
+
);
|
|
111
|
+
if (staged || committed) {
|
|
112
|
+
// A duplicate nav path within one plugin is an author mistake — two
|
|
113
|
+
// entries pointing at the same page — so fail loudly like the page guard.
|
|
114
|
+
throw new Error(
|
|
115
|
+
`plugin “${pluginId}” already registered a nav entry at “${contribution.path}”`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
stagedNavEntries.push({ ...contribution, pluginId });
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
await entry(context);
|
|
124
|
+
} finally {
|
|
125
|
+
// Whether `entry` resolved or threw, no further registration is valid — seal
|
|
126
|
+
// so a deferred call fails loudly rather than pushing into an orphaned array.
|
|
127
|
+
sealed = true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Every nav entry must link to a page THIS plugin registers — staged in this
|
|
131
|
+
// call or already committed. A nav entry with no matching page is a guaranteed
|
|
132
|
+
// dead link (the plugin page surface renders "Page not found"), so reject it
|
|
133
|
+
// loudly. This throws BEFORE the commit loops, so it propagates out of
|
|
134
|
+
// `loadPlugin` and NOTHING (pages, panels, tabs, nav) commits — atomicity holds.
|
|
135
|
+
for (const navEntry of stagedNavEntries) {
|
|
136
|
+
const hasPage =
|
|
137
|
+
stagedPages.some((p) => p.path === navEntry.path) ||
|
|
138
|
+
pages.some((p) => p.pluginId === pluginId && p.path === navEntry.path);
|
|
139
|
+
if (!hasPage) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`plugin “${pluginId}” registered a nav entry at “${navEntry.path}” but no page at that path`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// `entry` resolved without throwing: commit the staged contributions as one
|
|
147
|
+
// batch, so the registry only ever holds a plugin's complete set — never a
|
|
148
|
+
// partial set left behind by a mid-registration failure.
|
|
149
|
+
for (const page of stagedPages) {
|
|
150
|
+
pages.push(page);
|
|
151
|
+
}
|
|
152
|
+
for (const [toolName, contribution] of stagedToolPanels) {
|
|
153
|
+
toolPanels.set(toolName, contribution);
|
|
154
|
+
}
|
|
155
|
+
for (const tab of stagedSettingsTabs) {
|
|
156
|
+
settingsTabs.push(tab);
|
|
157
|
+
}
|
|
158
|
+
for (const navEntry of stagedNavEntries) {
|
|
159
|
+
navEntries.push(navEntry);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The shell reads this after loading every installed plugin bundle. */
|
|
164
|
+
export function getContributions(): PluginContributions {
|
|
165
|
+
return { toolPanels, pages, settingsTabs, navEntries };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Tests reset the module-level registry between cases. */
|
|
169
|
+
export function __resetContributions(): void {
|
|
170
|
+
toolPanels.clear();
|
|
171
|
+
pages.length = 0;
|
|
172
|
+
settingsTabs.length = 0;
|
|
173
|
+
navEntries.length = 0;
|
|
174
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Studio-plugin authoring contract. A platform plugin ships an ESM bundle
|
|
3
|
+
* that exports a `register(context)` entry ({@link PluginEntry}); the host calls
|
|
4
|
+
* it with a {@link PluginContext} through which the plugin contributes tool
|
|
5
|
+
* panels, pages, settings tabs, and sidebar nav entries.
|
|
6
|
+
*
|
|
7
|
+
* Two styling paths are sanctioned, and only these two:
|
|
8
|
+
* 1. SDK components plus inline styles that read the design-system tokens
|
|
9
|
+
* (`var(--tai-*)`). Raw Tailwind utilities are host-internal, not part of
|
|
10
|
+
* this contract.
|
|
11
|
+
* 2. A plugin-shipped SCOPED stylesheet, listed in the bundle's manifest
|
|
12
|
+
* `integrity` map as a `.css` asset and HOST-injected as an SRI'd
|
|
13
|
+
* `<link rel="stylesheet">` before the bundle's JS is imported. It is bound
|
|
14
|
+
* by three hard rules: (a) no global resets/preflight — never style `html`,
|
|
15
|
+
* `body`, `:root`, `*`, or bare element selectors at top level; (b) every
|
|
16
|
+
* selector is scoped under a plugin root class the plugin renders itself,
|
|
17
|
+
* prefixed with the plugin's own package name; (c) theme values in
|
|
18
|
+
* PLUGIN-AUTHORED rules come from the SDK custom properties (`var(--tai-*)`),
|
|
19
|
+
* never hardcoded colors, so the stylesheet themes itself with no logic of
|
|
20
|
+
* its own. Bundled third-party base CSS is exempt from rule (c) but rules
|
|
21
|
+
* (a) and (b) still bind it.
|
|
22
|
+
*/
|
|
23
|
+
import type { ComponentType } from 'react';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A capability requirement a contribution may declare. `routes` is a list of
|
|
27
|
+
* route-path prefixes (anyOf semantics, the same evaluator the shell nav uses):
|
|
28
|
+
* the contribution renders iff the caller's capability projection covers at least
|
|
29
|
+
* one of them. ABSENT (the field is optional) means the contribution renders only
|
|
30
|
+
* for a FULL projection — safe-by-default for every existing plugin, which never
|
|
31
|
+
* declared a requirement. The server remains the authority; this only shapes what
|
|
32
|
+
* the UI advertises.
|
|
33
|
+
*/
|
|
34
|
+
export interface RequiredCapabilities {
|
|
35
|
+
readonly routes: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Props a contributed tool-run panel receives (replaces the auto-form). */
|
|
39
|
+
export interface ToolPanelProps {
|
|
40
|
+
/** The tool name this panel targets. */
|
|
41
|
+
readonly toolName: string;
|
|
42
|
+
/** The tool's JSON schema (Pydantic-emitted), for panels that introspect it. */
|
|
43
|
+
readonly schema: Record<string, unknown>;
|
|
44
|
+
/** Run the tool with arguments; resolves with the typed result or throws. */
|
|
45
|
+
readonly run: (args: Record<string, unknown>) => Promise<unknown>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Props a contributed full page receives. Navigation is via shell route tokens. */
|
|
49
|
+
export interface PluginPageProps {
|
|
50
|
+
readonly pluginId: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Props a contributed settings tab receives. */
|
|
54
|
+
export interface SettingsTabProps {
|
|
55
|
+
readonly pluginId: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ToolPanelContribution {
|
|
59
|
+
readonly toolName: string;
|
|
60
|
+
readonly component: ComponentType<ToolPanelProps>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface PageContribution {
|
|
64
|
+
/** URL segment under `/plugins/{pluginId}/`. */
|
|
65
|
+
readonly path: string;
|
|
66
|
+
readonly title: string;
|
|
67
|
+
readonly component: ComponentType<PluginPageProps>;
|
|
68
|
+
/** Capability gate (see {@link RequiredCapabilities}); absent ⇒ full-only. */
|
|
69
|
+
readonly requiredCapabilities?: RequiredCapabilities;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A page as stored in the registry: the plugin's {@link PageContribution} plus
|
|
74
|
+
* the id of the plugin that registered it. The `pluginId` is stamped by the
|
|
75
|
+
* registry from the identity the host passed to {@link PluginEntry} — a plugin
|
|
76
|
+
* never supplies its own id — so a page resolves only under its owner's
|
|
77
|
+
* `/plugins/{pluginId}/` prefix and two plugins may register the same `path`
|
|
78
|
+
* without colliding.
|
|
79
|
+
*/
|
|
80
|
+
export interface RegisteredPage extends PageContribution {
|
|
81
|
+
readonly pluginId: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface NavEntryContribution {
|
|
85
|
+
/**
|
|
86
|
+
* Path of a page THIS plugin registers. The nav entry links to
|
|
87
|
+
* `/plugins/{pluginId}/{path}`, so it must match a {@link PageContribution}
|
|
88
|
+
* `path` of the same plugin — the registry rejects a nav entry with no page.
|
|
89
|
+
*/
|
|
90
|
+
readonly path: string;
|
|
91
|
+
readonly title: string;
|
|
92
|
+
/**
|
|
93
|
+
* Optional icon rendered before the title. It must be a square inline SVG that
|
|
94
|
+
* fills its box and draws with `currentColor`; the host constrains the slot,
|
|
95
|
+
* rendering it inside a fixed 1em box, `aria-hidden` (the accessible name is
|
|
96
|
+
* `title`), with the color inherited from the link. Its bytes live inside the
|
|
97
|
+
* plugin bundle, so no external fetch and no CSP change. Absent icon renders a
|
|
98
|
+
* text-only entry, exactly like the core nav.
|
|
99
|
+
*/
|
|
100
|
+
readonly icon?: ComponentType;
|
|
101
|
+
/** Capability gate (see {@link RequiredCapabilities}); absent ⇒ full-only. */
|
|
102
|
+
readonly requiredCapabilities?: RequiredCapabilities;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A nav entry as stored in the registry: the plugin's
|
|
107
|
+
* {@link NavEntryContribution} plus the id of the plugin that registered it. The
|
|
108
|
+
* `pluginId` is stamped by the registry from the identity the host passed to
|
|
109
|
+
* {@link PluginEntry}, so the entry links only under its owner's
|
|
110
|
+
* `/plugins/{pluginId}/` prefix and two plugins may register the same `path`
|
|
111
|
+
* without colliding.
|
|
112
|
+
*/
|
|
113
|
+
export interface RegisteredNavEntry extends NavEntryContribution {
|
|
114
|
+
readonly pluginId: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface SettingsTabContribution {
|
|
118
|
+
readonly id: string;
|
|
119
|
+
readonly title: string;
|
|
120
|
+
readonly component: ComponentType<SettingsTabProps>;
|
|
121
|
+
/** Capability gate (see {@link RequiredCapabilities}); absent ⇒ full-only. */
|
|
122
|
+
readonly requiredCapabilities?: RequiredCapabilities;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A settings tab as stored in the registry: the plugin's
|
|
127
|
+
* {@link SettingsTabContribution} plus the id of the plugin that registered it.
|
|
128
|
+
* The `pluginId` is stamped by the registry from the identity the host passed to
|
|
129
|
+
* {@link PluginEntry} — a plugin never supplies its own id — so a tab is always
|
|
130
|
+
* attributable to its owner and two plugins may register the same tab `id`
|
|
131
|
+
* without colliding.
|
|
132
|
+
*/
|
|
133
|
+
export interface RegisteredSettingsTab extends SettingsTabContribution {
|
|
134
|
+
readonly pluginId: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Everything a single plugin bundle has registered at import time. */
|
|
138
|
+
export interface PluginContributions {
|
|
139
|
+
readonly toolPanels: ReadonlyMap<string, ToolPanelContribution>;
|
|
140
|
+
readonly pages: readonly RegisteredPage[];
|
|
141
|
+
readonly settingsTabs: readonly RegisteredSettingsTab[];
|
|
142
|
+
readonly navEntries: readonly RegisteredNavEntry[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The registration surface a plugin receives in its `register(context)` entry.
|
|
147
|
+
* Each method binds the contribution to the plugin the host is loading — the
|
|
148
|
+
* plugin's identity is fixed by the host, not read from any ambient state — so a
|
|
149
|
+
* contribution can never be misattributed to another plugin. Contributions are
|
|
150
|
+
* staged and committed together once `register` settles; a `register` that throws
|
|
151
|
+
* commits nothing. The context is SEALED once `register` settles: a call made
|
|
152
|
+
* after that (a deferred timer, a post-resolve microtask) throws instead of
|
|
153
|
+
* silently dropping, so every registration must happen during `register`.
|
|
154
|
+
*/
|
|
155
|
+
export interface PluginContext {
|
|
156
|
+
/** Register a full page mounted under `/plugins/{pluginId}/{path}`. */
|
|
157
|
+
registerPage(contribution: PageContribution): void;
|
|
158
|
+
/** Register a rich run panel for a tool by name (overrides the auto-form). */
|
|
159
|
+
registerToolPanel(contribution: ToolPanelContribution): void;
|
|
160
|
+
/** Register a settings tab. */
|
|
161
|
+
registerSettingsTab(contribution: SettingsTabContribution): void;
|
|
162
|
+
/**
|
|
163
|
+
* Register a sidebar nav entry linking to one of this plugin's pages at
|
|
164
|
+
* `/plugins/{pluginId}/{path}`. The `path` must match a page the plugin also
|
|
165
|
+
* registers (a nav entry with no page is a dead link and is rejected loudly).
|
|
166
|
+
*/
|
|
167
|
+
registerNavEntry(contribution: NavEntryContribution): void;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A plugin bundle's entry: the default plugin API. The host imports the bundle,
|
|
172
|
+
* reads its `register` export, and calls it with a {@link PluginContext}. All
|
|
173
|
+
* contributions flow through that context — there are no free registration
|
|
174
|
+
* functions. The entry may be synchronous or `async`; the host awaits it before
|
|
175
|
+
* committing, so an async entry must complete every registration before it
|
|
176
|
+
* resolves (see the seal on {@link PluginContext}).
|
|
177
|
+
*/
|
|
178
|
+
export type PluginEntry = (context: PluginContext) => void | Promise<void>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Studio-plugin API compatibility version.
|
|
3
|
+
*
|
|
4
|
+
* A monotonic integer bumped ONLY on a breaking change to the Studio-plugin API
|
|
5
|
+
* OR the public prop/type surface of any DS component the SDK exports (a prop
|
|
6
|
+
* rename/removal bumps it; genuinely internal/visual changes do not).
|
|
7
|
+
*
|
|
8
|
+
* Gate rule: equality is correct precisely because additive changes never
|
|
9
|
+
* bump (existing plugins keep matching) and after a break every plugin must
|
|
10
|
+
* rebuild by design. `targeted == current` accepts; anything else rejects.
|
|
11
|
+
*
|
|
12
|
+
* Additive evolution under this gate: a host that gains a NEW optional context
|
|
13
|
+
* capability (a new `register*` method, a new host-side injection behavior)
|
|
14
|
+
* keeps the same version, so every existing plugin loads unchanged. The one
|
|
15
|
+
* asymmetry the equality gate leaves is a plugin that USES a newer capability on
|
|
16
|
+
* an OLDER host that lacks it — and it fails loudly, by construction, not by any
|
|
17
|
+
* negotiation machinery:
|
|
18
|
+
* - A plugin calling a context method the host does not define throws the
|
|
19
|
+
* natural `context.<method> is not a function` during `register`; the loader
|
|
20
|
+
* catches it as that plugin's loud error card and commits nothing.
|
|
21
|
+
* - A plugin shipping a `.css` asset to a host that does not inject stylesheets
|
|
22
|
+
* is served but never injected — the page renders with SDK-component styling
|
|
23
|
+
* only. This is the single quiet degradation the mechanism admits, and the
|
|
24
|
+
* authoring docs state it: stylesheet injection is a host capability, so a
|
|
25
|
+
* deployment pairs the Studio host with plugins built for it.
|
|
26
|
+
*/
|
|
27
|
+
export const STUDIO_PLUGIN_API_VERSION = 1;
|
|
28
|
+
|
|
29
|
+
export interface VersionGateResult {
|
|
30
|
+
readonly ok: boolean;
|
|
31
|
+
readonly reason?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Accept iff the plugin's targeted version exactly equals the host's. */
|
|
35
|
+
export function checkPluginApiVersion(
|
|
36
|
+
targeted: number,
|
|
37
|
+
current = STUDIO_PLUGIN_API_VERSION,
|
|
38
|
+
): VersionGateResult {
|
|
39
|
+
if (targeted === current) return { ok: true };
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
reason: `plugin targets Studio API v${String(targeted)} but this host is v${String(current)} — the plugin must be rebuilt`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-feature TanStack Query keys.
|
|
3
|
+
*
|
|
4
|
+
* Feature packages cannot import each other — the SDK is their one shared seam — so
|
|
5
|
+
* a query key more than one feature must reference lives here as a single
|
|
6
|
+
* authoritative constant rather than a duplicated literal that could drift.
|
|
7
|
+
*
|
|
8
|
+
* `toolsListKey` is the registered-tool master list. A mutation that changes the
|
|
9
|
+
* registered-tool population — creating, deleting, or rolling back a preset (each
|
|
10
|
+
* binds or tears down a live tool) — invalidates it so the tools page refetches.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** The registered-tool master list (`GET /api/tools`). */
|
|
14
|
+
export const toolsListKey = ['tools'] as const;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The extension catalog (`GET /api/extensions`). The extensions feature keys its
|
|
18
|
+
* read-only catalog on it; a tool's extension-combo save (in the tools feature)
|
|
19
|
+
* creates or tears down branch tools, so it invalidates this key too — a
|
|
20
|
+
* cross-feature reference that must resolve to one authoritative constant.
|
|
21
|
+
*/
|
|
22
|
+
export const extensionsQueryKey = ['extensions'] as const;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The discovered sub-MCP mounts (`GET /api/sub-mcp`). The settings feature reads
|
|
26
|
+
* this map (its scope mapper lists the mounts) while the manifest feature owns the
|
|
27
|
+
* create/delete controls that mutate it; both must key and invalidate the exact
|
|
28
|
+
* same tuple, so it lives here rather than as a literal duplicated across the two
|
|
29
|
+
* features (which cannot import each other).
|
|
30
|
+
*/
|
|
31
|
+
export const subMcpKey = ['sub-mcp'] as const;
|
|
32
|
+
|
|
33
|
+
/** The api-key payloads; read by settings AND the hooks execution-key picker. */
|
|
34
|
+
export const tokensPayloadKey = ['auth-tokens-payload'] as const;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SchemaEditor` — the shared control for AUTHORING a JSON Schema. It is a VALIDATED
|
|
3
|
+
* code editor, NOT a visual builder: the operator writes the schema as JSON in a
|
|
4
|
+
* textarea, and the editor gives loud parse feedback, a structural lint (top-level
|
|
5
|
+
* object; a `"title"` when `requireTitle`), and a live PREVIEW of the authored shape
|
|
6
|
+
* through the same {@link SchemaForm} renderer the run side uses (falling back to a
|
|
7
|
+
* {@link JsonTree} of the schema dict when the shape is not form-renderable).
|
|
8
|
+
*
|
|
9
|
+
* A JSON Schema is more expressive than any builder UI (`$defs`, `anyOf`, constraint
|
|
10
|
+
* keywords), and the backend rejects an invalid schema LOUDLY — so full meta-schema
|
|
11
|
+
* validation stays server-side and this control adds no schema-validation dependency.
|
|
12
|
+
*
|
|
13
|
+
* CONTROLLED-BY-SEED: `value` seeds the editor's text ONCE (on mount); thereafter the
|
|
14
|
+
* text is the source of truth and every edit reports up through `onChange`. Validity
|
|
15
|
+
* is exposed there too so the consumer can gate its submit — always alongside a
|
|
16
|
+
* VISIBLE inline message, never a silently disabled button. The parsed dict is passed
|
|
17
|
+
* through untouched, so a schema the editor does not understand round-trips exactly.
|
|
18
|
+
*
|
|
19
|
+
* SAFETY: the schema text is operator-authored and every derived value renders as
|
|
20
|
+
* React TEXT (through the DS controls / `JsonTree` / `SchemaForm`), never an HTML sink.
|
|
21
|
+
*/
|
|
22
|
+
import { Component, useMemo, useState, type ReactNode } from 'react';
|
|
23
|
+
|
|
24
|
+
import { Field } from '../components/field';
|
|
25
|
+
import { Textarea } from '../components/inputs';
|
|
26
|
+
import { JsonTree } from '../components/json-tree';
|
|
27
|
+
import { ScrollRegion } from '../components/scroll-region';
|
|
28
|
+
import { classifySchema } from '../schema-form/classify';
|
|
29
|
+
import { defaultValueForSchema } from '../schema-form/default-value';
|
|
30
|
+
import { SchemaForm } from '../schema-form/SchemaForm';
|
|
31
|
+
import type { JsonSchema } from '../schema-form/types';
|
|
32
|
+
import { lintSchemaText } from './lint';
|
|
33
|
+
|
|
34
|
+
/** What {@link SchemaEditor} reports up on every edit. */
|
|
35
|
+
export interface SchemaEditorChange {
|
|
36
|
+
/** The authored schema dict when valid and non-empty; `null` when the editor is empty. */
|
|
37
|
+
readonly schema: Record<string, unknown> | null;
|
|
38
|
+
/** False when the text is non-empty but unparseable or failing lint — gate submit on this. */
|
|
39
|
+
readonly valid: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SchemaEditorProps {
|
|
43
|
+
/** The schema dict to seed the editor with, or `null` for an empty editor. */
|
|
44
|
+
readonly value: Record<string, unknown> | null;
|
|
45
|
+
/** Fired on every edit with the parsed schema (or `null`) and its validity. */
|
|
46
|
+
readonly onChange: (change: SchemaEditorChange) => void;
|
|
47
|
+
/** When true, a top-level `"title"` is required (the `response_format` contract). */
|
|
48
|
+
readonly requireTitle: boolean;
|
|
49
|
+
readonly label?: string;
|
|
50
|
+
readonly description?: string;
|
|
51
|
+
readonly disabled?: boolean;
|
|
52
|
+
readonly idPrefix?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function seedText(value: Record<string, unknown> | null): string {
|
|
56
|
+
return value === null ? '' : JSON.stringify(value, null, 2);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether {@link SchemaForm} can render this schema's root — i.e. it classifies to a
|
|
61
|
+
* concrete field kind rather than `unsupported`. A schema whose root the form cannot
|
|
62
|
+
* render (or whose classification throws on a bad `$ref`) previews as a `JsonTree`.
|
|
63
|
+
*/
|
|
64
|
+
function canRenderWithForm(schema: JsonSchema): boolean {
|
|
65
|
+
try {
|
|
66
|
+
return classifySchema(schema, schema).model.kind !== 'unsupported';
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* An interactive preview of the authored schema. Seeds a throwaway value from the
|
|
74
|
+
* schema's defaults so the operator can see (and poke) the shape they defined.
|
|
75
|
+
*/
|
|
76
|
+
function PreviewForm({ schema }: { readonly schema: JsonSchema }): ReactNode {
|
|
77
|
+
const [value, setValue] = useState<unknown>(() => defaultValueForSchema(schema));
|
|
78
|
+
return <SchemaForm schema={schema} value={value} onChange={setValue} idPrefix="schema-preview" />;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Catch a render-time throw from a deep, malformed schema (e.g. a nested unresolvable
|
|
83
|
+
* `$ref`) so the preview degrades to the `JsonTree` fallback instead of crashing the
|
|
84
|
+
* enclosing dialog. Reset per authored schema by keying this boundary on the schema.
|
|
85
|
+
*/
|
|
86
|
+
class PreviewBoundary extends Component<
|
|
87
|
+
{ readonly fallback: ReactNode; readonly children: ReactNode },
|
|
88
|
+
{ readonly failed: boolean }
|
|
89
|
+
> {
|
|
90
|
+
override state = { failed: false };
|
|
91
|
+
static getDerivedStateFromError(_error: unknown): { failed: boolean } {
|
|
92
|
+
return { failed: true };
|
|
93
|
+
}
|
|
94
|
+
override render(): ReactNode {
|
|
95
|
+
return this.state.failed ? this.props.fallback : this.props.children;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The preview pane's accessible name, wherever the scrolling box turns out to be. */
|
|
100
|
+
const PREVIEW_LABEL = 'Schema preview';
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The preview pane, framed as a card and scrollable — a deep schema's preview can
|
|
104
|
+
* outrun its column, so it scrolls in place rather than pushing the dialog sideways.
|
|
105
|
+
*
|
|
106
|
+
* WHICH element scrolls depends on the branch, and only the one that actually
|
|
107
|
+
* scrolls may carry the region attributes. {@link JsonTree} IS its own scrolling
|
|
108
|
+
* box, so on the fallback branch the card is a plain frame and the tree names
|
|
109
|
+
* itself; a `ScrollRegion` around it would take an overflow that never reaches it,
|
|
110
|
+
* leaving a name announced for a box that cannot move. The {@link SchemaForm}
|
|
111
|
+
* branch renders no scroller of its own, so there the card IS the region.
|
|
112
|
+
*/
|
|
113
|
+
function SchemaPreview({
|
|
114
|
+
schema,
|
|
115
|
+
testId,
|
|
116
|
+
}: {
|
|
117
|
+
readonly schema: Record<string, unknown>;
|
|
118
|
+
readonly testId: string;
|
|
119
|
+
}): ReactNode {
|
|
120
|
+
const jsonFallback = (
|
|
121
|
+
<div className="tai-card" data-testid={testId}>
|
|
122
|
+
<JsonTree data={schema} label={PREVIEW_LABEL} />
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
if (!canRenderWithForm(schema)) return jsonFallback;
|
|
126
|
+
return (
|
|
127
|
+
<PreviewBoundary fallback={jsonFallback}>
|
|
128
|
+
<ScrollRegion label={PREVIEW_LABEL} className="tai-card" data-testid={testId}>
|
|
129
|
+
<PreviewForm schema={schema} />
|
|
130
|
+
</ScrollRegion>
|
|
131
|
+
</PreviewBoundary>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function SchemaEditor({
|
|
136
|
+
value,
|
|
137
|
+
onChange,
|
|
138
|
+
requireTitle,
|
|
139
|
+
label = 'Schema',
|
|
140
|
+
description,
|
|
141
|
+
disabled,
|
|
142
|
+
idPrefix = 'schema-editor',
|
|
143
|
+
}: SchemaEditorProps): ReactNode {
|
|
144
|
+
const [text, setText] = useState<string>(() => seedText(value));
|
|
145
|
+
|
|
146
|
+
// Recomputed for display (inline error + preview) on every render; the change
|
|
147
|
+
// handler lints the NEXT text independently so `onChange` and the view agree.
|
|
148
|
+
const result = useMemo(() => lintSchemaText(text, requireTitle), [text, requireTitle]);
|
|
149
|
+
|
|
150
|
+
const emit = (nextText: string): void => {
|
|
151
|
+
setText(nextText);
|
|
152
|
+
const next = lintSchemaText(nextText, requireTitle);
|
|
153
|
+
onChange({ schema: next.valid ? next.schema : null, valid: next.valid });
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div data-testid={idPrefix} className="tai-stack tai-stack-3">
|
|
158
|
+
<Field label={label} description={description} error={result.error}>
|
|
159
|
+
<Textarea
|
|
160
|
+
value={text}
|
|
161
|
+
disabled={disabled}
|
|
162
|
+
onChange={(event) => {
|
|
163
|
+
emit(event.target.value);
|
|
164
|
+
}}
|
|
165
|
+
rows={10}
|
|
166
|
+
aria-label={`${label} JSON`}
|
|
167
|
+
className="tai-textarea-mono"
|
|
168
|
+
spellCheck={false}
|
|
169
|
+
/>
|
|
170
|
+
</Field>
|
|
171
|
+
|
|
172
|
+
{result.schema !== null ? (
|
|
173
|
+
<div className="tai-stack tai-stack-2">
|
|
174
|
+
<span className="tai-label">Preview</span>
|
|
175
|
+
<SchemaPreview
|
|
176
|
+
// Re-seed the preview (and reset its error boundary) whenever the
|
|
177
|
+
// authored schema changes.
|
|
178
|
+
key={JSON.stringify(result.schema)}
|
|
179
|
+
schema={result.schema}
|
|
180
|
+
testId={`${idPrefix}-preview`}
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
) : null}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-editor — the shared, validated JSON-Schema authoring control. Public
|
|
3
|
+
* surface: the `SchemaEditor` component, its change type, and the pure lint helper
|
|
4
|
+
* consumers reuse to pre-check a stored schema.
|
|
5
|
+
*/
|
|
6
|
+
export { SchemaEditor } from './SchemaEditor';
|
|
7
|
+
export type { SchemaEditorProps, SchemaEditorChange } from './SchemaEditor';
|
|
8
|
+
export { lintSchemaText } from './lint';
|
|
9
|
+
export type { SchemaLintResult } from './lint';
|