@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,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure parse + structural lint behind the `SchemaEditor` module. It never throws:
|
|
3
|
+
* a malformed value IS the result it reports. Full meta-schema validation stays
|
|
4
|
+
* server-side (the backend 400s carry the detail); this is the fast, loud client
|
|
5
|
+
* guard that keeps an obviously-broken schema from ever leaving the dialog.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** The parse/lint outcome for one editor text buffer. */
|
|
9
|
+
export interface SchemaLintResult {
|
|
10
|
+
/**
|
|
11
|
+
* The parsed schema dict when the text is a syntactically valid JSON object, or
|
|
12
|
+
* `null` when the text is empty or does not parse. Carries the parsed object even
|
|
13
|
+
* when a lint (e.g. a missing `title`) fails, so the preview can still show the
|
|
14
|
+
* authored shape — the consumer gates on {@link valid}, never on this alone.
|
|
15
|
+
*/
|
|
16
|
+
readonly schema: Record<string, unknown> | null;
|
|
17
|
+
/**
|
|
18
|
+
* True when the text is empty or a valid schema. False when the text is non-empty
|
|
19
|
+
* but fails to parse or fails a structural lint — the consumer blocks submit.
|
|
20
|
+
*/
|
|
21
|
+
readonly valid: boolean;
|
|
22
|
+
/** A loud, human-readable message when {@link valid} is false; else `undefined`. */
|
|
23
|
+
readonly error: string | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
27
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse and structurally lint a JSON-Schema editor buffer.
|
|
32
|
+
*
|
|
33
|
+
* - Empty text is VALID (an unset, optional schema) with a `null` schema.
|
|
34
|
+
* - Malformed JSON is INVALID; the parser message is surfaced verbatim.
|
|
35
|
+
* - A top-level value that is not a JSON object (an array or a scalar) is INVALID.
|
|
36
|
+
* - When `requireTitle`, a top-level string `"title"` is REQUIRED (the
|
|
37
|
+
* `response_format` contract the backend enforces); its absence is INVALID.
|
|
38
|
+
*
|
|
39
|
+
* The parsed object is returned untouched — no key is normalized, reordered, or
|
|
40
|
+
* dropped — so a schema carrying `$defs`/`anyOf` round-trips exactly.
|
|
41
|
+
*/
|
|
42
|
+
export function lintSchemaText(text: string, requireTitle: boolean): SchemaLintResult {
|
|
43
|
+
const trimmed = text.trim();
|
|
44
|
+
if (trimmed === '') return { schema: null, valid: true, error: undefined };
|
|
45
|
+
|
|
46
|
+
let parsed: unknown;
|
|
47
|
+
try {
|
|
48
|
+
parsed = JSON.parse(trimmed);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return {
|
|
51
|
+
schema: null,
|
|
52
|
+
valid: false,
|
|
53
|
+
error: error instanceof Error ? error.message : 'Invalid JSON.',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!isPlainObject(parsed)) {
|
|
58
|
+
return { schema: null, valid: false, error: 'The schema must be a JSON object.' };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (requireTitle && typeof parsed.title !== 'string') {
|
|
62
|
+
return {
|
|
63
|
+
schema: parsed,
|
|
64
|
+
valid: false,
|
|
65
|
+
error: 'A top-level "title" string is required for this schema.',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { schema: parsed, valid: true, error: undefined };
|
|
70
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SchemaForm` — the controlled, JSON-Schema-driven auto-form. Given a schema
|
|
3
|
+
* (as Pydantic v2 emits) plus a value and `onChange`, it renders the form and
|
|
4
|
+
* emits a plain JS value; the CALLER owns the submit button. It is the
|
|
5
|
+
* load-bearing renderer reused by the tools run panel and the interactions form
|
|
6
|
+
* answer format, which is why it lives in the SDK (features cannot import each
|
|
7
|
+
* other).
|
|
8
|
+
*
|
|
9
|
+
* SAFETY: every schema-derived string (title, description, enum label, notice)
|
|
10
|
+
* is rendered as TEXT through the DS components — React escapes it. No markdown,
|
|
11
|
+
* no HTML, no `dangerouslySetInnerHTML` sink ever touches a schema-supplied
|
|
12
|
+
* string. Pinned by a test.
|
|
13
|
+
*
|
|
14
|
+
* MEDIA UPLOADS: a string field renders as a file-upload / drag-drop control
|
|
15
|
+
* (instead of a text box) when its schema carries media annotations, so an
|
|
16
|
+
* image/file input stays a JSON-serializable string that round-trips through the
|
|
17
|
+
* normal submit path. The annotation contract:
|
|
18
|
+
* - `contentEncoding: "base64"` + `contentMediaType` (e.g. `"image/*"`,
|
|
19
|
+
* `"application/pdf"`) → the emitted value is the raw base64 body (no
|
|
20
|
+
* `data:` prefix); `contentMediaType` also constrains the accepted MIME.
|
|
21
|
+
* - `format: "data-url"` (optionally with `contentMediaType`) → the emitted
|
|
22
|
+
* value is a full `data:<mime>;base64,<body>` URL.
|
|
23
|
+
* - `contentMaxBytes` pins a per-field size cap (bytes); otherwise the
|
|
24
|
+
* {@link SchemaFormProps.maxUploadBytes} prop, else {@link DEFAULT_MAX_UPLOAD_BYTES}.
|
|
25
|
+
* An over-cap value or a MIME that does not match `contentMediaType` is REJECTED
|
|
26
|
+
* with a visible error — never silently truncated or accepted. The cap holds on
|
|
27
|
+
* EVERY input path (file picker, the paste fallback, and `validateAgainstSchema`),
|
|
28
|
+
* measured against the value's DECODED byte size. A text fallback (paste a
|
|
29
|
+
* URL/base64) stays available. The uploaded filename renders ESCAPED.
|
|
30
|
+
*
|
|
31
|
+
* `contentMaxBytes` is a NONSTANDARD JSON-Schema extension keyword (a standard
|
|
32
|
+
* validator ignores it); these client-side caps are UX guards, and the SERVER
|
|
33
|
+
* MUST independently validate the decoded size. The base64 decoding and cap
|
|
34
|
+
* checks live in the sibling `media` module.
|
|
35
|
+
*/
|
|
36
|
+
import type { ReactNode } from 'react';
|
|
37
|
+
|
|
38
|
+
import { CompletionProviderContext, MaxUploadBytesContext } from './context';
|
|
39
|
+
import { FieldNode } from './field-node';
|
|
40
|
+
import { DEFAULT_MAX_UPLOAD_BYTES } from './media';
|
|
41
|
+
import { stackClass } from './styles';
|
|
42
|
+
import type { JsonSchema, SchemaFormErrors } from './types';
|
|
43
|
+
|
|
44
|
+
// -- Public API --------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Fetch argument-value suggestions for a string field. `argName` is the field's
|
|
48
|
+
* path within the form (a property key at the root, a dotted path when nested);
|
|
49
|
+
* `partial` is the value typed so far. The caller owns the source (e.g. an MCP
|
|
50
|
+
* `completion/complete` call) — SchemaForm only routes string fields through the
|
|
51
|
+
* completion-backed input when a provider is supplied.
|
|
52
|
+
*/
|
|
53
|
+
export type CompletionProvider = (argName: string, partial: string) => Promise<string[]>;
|
|
54
|
+
|
|
55
|
+
export interface SchemaFormProps {
|
|
56
|
+
readonly schema: JsonSchema;
|
|
57
|
+
readonly value: unknown;
|
|
58
|
+
readonly onChange: (value: unknown) => void;
|
|
59
|
+
readonly errors?: SchemaFormErrors;
|
|
60
|
+
readonly idPrefix?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Optional. When supplied, every string field renders through the
|
|
63
|
+
* completion-backed input (argument autocomplete); when omitted, string fields
|
|
64
|
+
* render as a plain text input — the default, unchanged behaviour.
|
|
65
|
+
*/
|
|
66
|
+
readonly completionProvider?: CompletionProvider;
|
|
67
|
+
/**
|
|
68
|
+
* Optional. Default byte cap for media-upload fields (see the module
|
|
69
|
+
* doc-comment). A field's own `contentMaxBytes` annotation overrides it;
|
|
70
|
+
* absent both, {@link DEFAULT_MAX_UPLOAD_BYTES} applies.
|
|
71
|
+
*/
|
|
72
|
+
readonly maxUploadBytes?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Render a controlled form for `schema`. The root is usually an object (a tool's
|
|
77
|
+
* input schema); a scalar/array/union root renders as a single field group.
|
|
78
|
+
*/
|
|
79
|
+
export function SchemaForm({
|
|
80
|
+
schema,
|
|
81
|
+
value,
|
|
82
|
+
onChange,
|
|
83
|
+
errors,
|
|
84
|
+
idPrefix = 'schema-form',
|
|
85
|
+
completionProvider,
|
|
86
|
+
maxUploadBytes = DEFAULT_MAX_UPLOAD_BYTES,
|
|
87
|
+
}: SchemaFormProps): ReactNode {
|
|
88
|
+
return (
|
|
89
|
+
<CompletionProviderContext.Provider value={completionProvider}>
|
|
90
|
+
<MaxUploadBytesContext.Provider value={maxUploadBytes}>
|
|
91
|
+
<div className={stackClass} data-testid={idPrefix}>
|
|
92
|
+
<FieldNode
|
|
93
|
+
schema={schema}
|
|
94
|
+
root={schema}
|
|
95
|
+
value={value}
|
|
96
|
+
onChange={onChange}
|
|
97
|
+
path=""
|
|
98
|
+
label={undefined}
|
|
99
|
+
required
|
|
100
|
+
errors={errors}
|
|
101
|
+
idPrefix={idPrefix}
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
</MaxUploadBytesContext.Provider>
|
|
105
|
+
</CompletionProviderContext.Provider>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The array field renderer. Each item is a nested {@link FieldNode}; edits map
|
|
3
|
+
* the list to a new array (add appends a seeded item, remove filters by index).
|
|
4
|
+
* The whole list re-renders as one controlled value, so index keys are correct.
|
|
5
|
+
*/
|
|
6
|
+
import type { ReactNode } from 'react';
|
|
7
|
+
|
|
8
|
+
import { CloseIcon } from '../components/icons';
|
|
9
|
+
import { Button } from '../components/primitives';
|
|
10
|
+
import { classifySchema } from './classify';
|
|
11
|
+
import { defaultValueForSchema } from './default-value';
|
|
12
|
+
import { FieldGroup } from './field-group';
|
|
13
|
+
import { FieldNode } from './field-node';
|
|
14
|
+
import type { JsonSchema, SchemaFormErrors } from './types';
|
|
15
|
+
|
|
16
|
+
export function ArrayField({
|
|
17
|
+
heading,
|
|
18
|
+
description,
|
|
19
|
+
error,
|
|
20
|
+
items,
|
|
21
|
+
root,
|
|
22
|
+
value,
|
|
23
|
+
onChange,
|
|
24
|
+
path,
|
|
25
|
+
errors,
|
|
26
|
+
idPrefix,
|
|
27
|
+
}: {
|
|
28
|
+
heading: string;
|
|
29
|
+
description: string | undefined;
|
|
30
|
+
error: string | undefined;
|
|
31
|
+
items: JsonSchema;
|
|
32
|
+
root: JsonSchema;
|
|
33
|
+
value: unknown;
|
|
34
|
+
onChange: (value: unknown) => void;
|
|
35
|
+
path: string;
|
|
36
|
+
errors: SchemaFormErrors | undefined;
|
|
37
|
+
idPrefix: string;
|
|
38
|
+
}): ReactNode {
|
|
39
|
+
const list = Array.isArray(value) ? (value as unknown[]) : [];
|
|
40
|
+
const addItem = (): void => {
|
|
41
|
+
onChange([...list, newItemValue(items, root)]);
|
|
42
|
+
};
|
|
43
|
+
const removeItem = (index: number): void => {
|
|
44
|
+
onChange(list.filter((_, position) => position !== index));
|
|
45
|
+
};
|
|
46
|
+
const setItem = (index: number, next: unknown): void => {
|
|
47
|
+
onChange(list.map((item, position) => (position === index ? next : item)));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<FieldGroup heading={heading} description={description} error={error} atRoot={false}>
|
|
52
|
+
{list.length === 0 ? <span className="tai-field-hint">No items</span> : null}
|
|
53
|
+
{list.map((item, index) => (
|
|
54
|
+
<div
|
|
55
|
+
// Index keys are correct here: items have no stable identity and the
|
|
56
|
+
// whole list re-renders as one controlled value on every edit.
|
|
57
|
+
key={index}
|
|
58
|
+
className="tai-row"
|
|
59
|
+
>
|
|
60
|
+
{/* The item takes the row's spare width; the Remove button keeps its own. */}
|
|
61
|
+
<div style={{ flex: 1 }}>
|
|
62
|
+
<FieldNode
|
|
63
|
+
schema={items}
|
|
64
|
+
root={root}
|
|
65
|
+
value={item}
|
|
66
|
+
onChange={(next) => {
|
|
67
|
+
setItem(index, next);
|
|
68
|
+
}}
|
|
69
|
+
path={`${path}[${String(index)}]`}
|
|
70
|
+
label={`Item ${String(index + 1)}`}
|
|
71
|
+
required
|
|
72
|
+
errors={errors}
|
|
73
|
+
idPrefix={idPrefix}
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
<Button
|
|
77
|
+
type="button"
|
|
78
|
+
variant="secondary"
|
|
79
|
+
aria-label={`Remove item ${String(index + 1)}`}
|
|
80
|
+
onClick={() => {
|
|
81
|
+
removeItem(index);
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<CloseIcon />
|
|
85
|
+
Remove
|
|
86
|
+
</Button>
|
|
87
|
+
</div>
|
|
88
|
+
))}
|
|
89
|
+
<div>
|
|
90
|
+
<Button type="button" variant="secondary" onClick={addItem}>
|
|
91
|
+
Add item
|
|
92
|
+
</Button>
|
|
93
|
+
</div>
|
|
94
|
+
</FieldGroup>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function newItemValue(schema: JsonSchema, root: JsonSchema): unknown {
|
|
99
|
+
const seeded = defaultValueForSchema(schema, root);
|
|
100
|
+
if (seeded !== undefined) return seeded;
|
|
101
|
+
const classified = classifySchema(schema, root);
|
|
102
|
+
switch (classified.model.kind) {
|
|
103
|
+
case 'string':
|
|
104
|
+
return '';
|
|
105
|
+
case 'boolean':
|
|
106
|
+
return false;
|
|
107
|
+
default:
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single source of truth for "what kind of field is this schema?". Both the
|
|
3
|
+
* renderer and the validator classify through here so a construct is treated
|
|
4
|
+
* identically everywhere. Classification resolves `$ref`, normalizes `anyOf`/
|
|
5
|
+
* `oneOf` (null-union → nullable single field; multi-member → variant selector),
|
|
6
|
+
* and lands every unrecognized construct on the explicit `unsupported` kind —
|
|
7
|
+
* a field is never silently dropped.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
isNullSchema,
|
|
11
|
+
resolveRef,
|
|
12
|
+
scalarLabel,
|
|
13
|
+
typeList,
|
|
14
|
+
unionMembers,
|
|
15
|
+
variantLabel,
|
|
16
|
+
variantTag,
|
|
17
|
+
type EnumOption,
|
|
18
|
+
} from './resolve';
|
|
19
|
+
import type { JsonSchema } from './types';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* How a string field accepts binary content as a JSON-serializable string.
|
|
23
|
+
* Derived from a field's JSON-Schema media annotations (see the `SchemaForm` module).
|
|
24
|
+
*/
|
|
25
|
+
export interface MediaUpload {
|
|
26
|
+
/**
|
|
27
|
+
* `base64` — the emitted value is the raw base64 body (no `data:` prefix),
|
|
28
|
+
* matching `contentEncoding: "base64"`. `data-url` — the emitted value is a
|
|
29
|
+
* full `data:<mime>;base64,<body>` URL, matching `format: "data-url"`.
|
|
30
|
+
*/
|
|
31
|
+
readonly encoding: 'base64' | 'data-url';
|
|
32
|
+
/**
|
|
33
|
+
* The `contentMediaType` constraint (e.g. `image/*`, `application/pdf`), used
|
|
34
|
+
* for MIME validation and the file picker's `accept`. `undefined` for a bare
|
|
35
|
+
* `format: "data-url"` field with no declared type.
|
|
36
|
+
*/
|
|
37
|
+
readonly mediaType: string | undefined;
|
|
38
|
+
/** A per-field byte cap from `contentMaxBytes`, if the schema pins one. */
|
|
39
|
+
readonly maxBytes: number | undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** One selectable variant of a union field. */
|
|
43
|
+
export interface UnionVariant {
|
|
44
|
+
readonly label: string;
|
|
45
|
+
/** The discriminator tag value, or `undefined` for a non-discriminated union. */
|
|
46
|
+
readonly tag: unknown;
|
|
47
|
+
/** The variant schema as written (unresolved — the renderer resolves it). */
|
|
48
|
+
readonly schema: JsonSchema;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The renderer-facing classification of a schema node. */
|
|
52
|
+
export type FieldModel =
|
|
53
|
+
| { readonly kind: 'const'; readonly value: unknown }
|
|
54
|
+
| { readonly kind: 'enum'; readonly options: readonly EnumOption[] }
|
|
55
|
+
| {
|
|
56
|
+
readonly kind: 'string';
|
|
57
|
+
readonly format: string | undefined;
|
|
58
|
+
/** Present when media annotations opt the field into the upload control. */
|
|
59
|
+
readonly media: MediaUpload | undefined;
|
|
60
|
+
}
|
|
61
|
+
| { readonly kind: 'number'; readonly integer: boolean }
|
|
62
|
+
| { readonly kind: 'boolean' }
|
|
63
|
+
| { readonly kind: 'array'; readonly items: JsonSchema }
|
|
64
|
+
| {
|
|
65
|
+
readonly kind: 'object';
|
|
66
|
+
readonly properties: readonly (readonly [string, JsonSchema])[];
|
|
67
|
+
readonly required: ReadonlySet<string>;
|
|
68
|
+
}
|
|
69
|
+
| {
|
|
70
|
+
readonly kind: 'union';
|
|
71
|
+
readonly variants: readonly UnionVariant[];
|
|
72
|
+
readonly discriminator: string | undefined;
|
|
73
|
+
}
|
|
74
|
+
| { readonly kind: 'unsupported'; readonly reason: string };
|
|
75
|
+
|
|
76
|
+
/** A fully classified field: its model plus resolved metadata. */
|
|
77
|
+
export interface ClassifiedField {
|
|
78
|
+
readonly model: FieldModel;
|
|
79
|
+
/** True when JSON `null` is an accepted value (a null-union member). */
|
|
80
|
+
readonly nullable: boolean;
|
|
81
|
+
/** The effective (ref-resolved) schema this classification describes. */
|
|
82
|
+
readonly schema: JsonSchema;
|
|
83
|
+
readonly title: string | undefined;
|
|
84
|
+
readonly description: string | undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function firstString(...values: readonly unknown[]): string | undefined {
|
|
88
|
+
for (const value of values) {
|
|
89
|
+
if (typeof value === 'string' && value.length > 0) return value;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Detect the media-upload annotations on a (ref-resolved) string schema. A field
|
|
96
|
+
* opts into the upload control when it carries `contentEncoding: "base64"` plus a
|
|
97
|
+
* `contentMediaType`, OR `format: "data-url"`. Anything else is a plain string.
|
|
98
|
+
*/
|
|
99
|
+
function mediaUpload(schema: JsonSchema): MediaUpload | undefined {
|
|
100
|
+
const mediaType =
|
|
101
|
+
typeof schema.contentMediaType === 'string' ? schema.contentMediaType : undefined;
|
|
102
|
+
const maxBytes =
|
|
103
|
+
typeof schema.contentMaxBytes === 'number' && schema.contentMaxBytes > 0
|
|
104
|
+
? schema.contentMaxBytes
|
|
105
|
+
: undefined;
|
|
106
|
+
if (schema.format === 'data-url') {
|
|
107
|
+
return { encoding: 'data-url', mediaType, maxBytes };
|
|
108
|
+
}
|
|
109
|
+
if (schema.contentEncoding === 'base64' && mediaType !== undefined) {
|
|
110
|
+
return { encoding: 'base64', mediaType, maxBytes };
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function unsupported(
|
|
116
|
+
schema: JsonSchema,
|
|
117
|
+
reason: string,
|
|
118
|
+
title: string | undefined,
|
|
119
|
+
description: string | undefined,
|
|
120
|
+
): ClassifiedField {
|
|
121
|
+
return { model: { kind: 'unsupported', reason }, nullable: false, schema, title, description };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Classify a schema node (resolving `$ref` first). `wrapper` metadata (a
|
|
126
|
+
* ref-site title/description) is threaded down so a `{ $ref, title }` or a
|
|
127
|
+
* null-union wrapper keeps its property-level label.
|
|
128
|
+
*/
|
|
129
|
+
export function classifySchema(raw: JsonSchema, root: JsonSchema): ClassifiedField {
|
|
130
|
+
const resolved = resolveRef(raw, root);
|
|
131
|
+
const title = firstString(raw.title, resolved.title);
|
|
132
|
+
const description = firstString(raw.description, resolved.description);
|
|
133
|
+
|
|
134
|
+
// const pins a single value.
|
|
135
|
+
if ('const' in resolved) {
|
|
136
|
+
return {
|
|
137
|
+
model: { kind: 'const', value: resolved.const },
|
|
138
|
+
nullable: false,
|
|
139
|
+
schema: resolved,
|
|
140
|
+
title,
|
|
141
|
+
description,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// enum → a fixed choice list.
|
|
146
|
+
if (resolved.enum !== undefined) {
|
|
147
|
+
const options: EnumOption[] = resolved.enum.map((value) => ({
|
|
148
|
+
value,
|
|
149
|
+
label: scalarLabel(value),
|
|
150
|
+
}));
|
|
151
|
+
const nullable = resolved.enum.some((value) => value === null);
|
|
152
|
+
return { model: { kind: 'enum', options }, nullable, schema: resolved, title, description };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// anyOf/oneOf union normalization.
|
|
156
|
+
const members = unionMembers(resolved);
|
|
157
|
+
if (members !== undefined) {
|
|
158
|
+
const nonNull = members.filter((member) => !isNullSchema(resolveRef(member, root)));
|
|
159
|
+
const nullable = nonNull.length !== members.length;
|
|
160
|
+
const soleNonNull = nonNull[0];
|
|
161
|
+
if (soleNonNull === undefined) {
|
|
162
|
+
return unsupported(resolved, 'union with no non-null members', title, description);
|
|
163
|
+
}
|
|
164
|
+
if (nonNull.length === 1) {
|
|
165
|
+
const inner = classifySchema(soleNonNull, root);
|
|
166
|
+
// Media annotations (`contentEncoding`/`contentMediaType`/`format: data-url`)
|
|
167
|
+
// that land on the OUTER null-union wrapper of an OPTIONAL field would
|
|
168
|
+
// otherwise be lost when delegating to the inner string member, silently
|
|
169
|
+
// degrading an optional upload field to a plain text box. Re-detect them on
|
|
170
|
+
// the wrapper and attach to the inner string model.
|
|
171
|
+
const model =
|
|
172
|
+
inner.model.kind === 'string' && inner.model.media === undefined
|
|
173
|
+
? { ...inner.model, media: mediaUpload(resolved) }
|
|
174
|
+
: inner.model;
|
|
175
|
+
return {
|
|
176
|
+
model,
|
|
177
|
+
nullable: nullable || inner.nullable,
|
|
178
|
+
schema: inner.schema,
|
|
179
|
+
title: title ?? inner.title,
|
|
180
|
+
description: description ?? inner.description,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const discriminator = resolved.discriminator?.propertyName;
|
|
184
|
+
const variants: UnionVariant[] = nonNull.map((member, index) => {
|
|
185
|
+
const tag = discriminator === undefined ? undefined : variantTag(member, discriminator, root);
|
|
186
|
+
return { label: variantLabel(member, index, tag, root), tag, schema: member };
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
model: { kind: 'union', variants, discriminator },
|
|
190
|
+
nullable,
|
|
191
|
+
schema: resolved,
|
|
192
|
+
title,
|
|
193
|
+
description,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// allOf: Pydantic wraps a single $ref this way; a multi-member intersection is
|
|
198
|
+
// not a form-renderable construct.
|
|
199
|
+
if (resolved.allOf !== undefined) {
|
|
200
|
+
const soleAllOf = resolved.allOf[0];
|
|
201
|
+
if (resolved.allOf.length === 1 && soleAllOf !== undefined) {
|
|
202
|
+
const inner = classifySchema(soleAllOf, root);
|
|
203
|
+
return {
|
|
204
|
+
model: inner.model,
|
|
205
|
+
nullable: inner.nullable,
|
|
206
|
+
schema: inner.schema,
|
|
207
|
+
title: title ?? inner.title,
|
|
208
|
+
description: description ?? inner.description,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return unsupported(resolved, 'allOf intersection of multiple schemas', title, description);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Type-driven classification.
|
|
215
|
+
const types = typeList(resolved);
|
|
216
|
+
const nonNullTypes = types.filter((type) => type !== 'null');
|
|
217
|
+
const nullableByType = nonNullTypes.length !== types.length;
|
|
218
|
+
|
|
219
|
+
if (nonNullTypes.length === 0) {
|
|
220
|
+
return unsupported(resolved, 'schema declares no renderable type', title, description);
|
|
221
|
+
}
|
|
222
|
+
if (nonNullTypes.length > 1) {
|
|
223
|
+
return unsupported(
|
|
224
|
+
resolved,
|
|
225
|
+
`multiple JSON types (${nonNullTypes.join(', ')}) without a discriminator`,
|
|
226
|
+
title,
|
|
227
|
+
description,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const type = nonNullTypes[0];
|
|
232
|
+
if (type === undefined) {
|
|
233
|
+
return unsupported(resolved, 'schema declares no renderable type', title, description);
|
|
234
|
+
}
|
|
235
|
+
switch (type) {
|
|
236
|
+
case 'string':
|
|
237
|
+
return {
|
|
238
|
+
model: { kind: 'string', format: resolved.format, media: mediaUpload(resolved) },
|
|
239
|
+
nullable: nullableByType,
|
|
240
|
+
schema: resolved,
|
|
241
|
+
title,
|
|
242
|
+
description,
|
|
243
|
+
};
|
|
244
|
+
case 'number':
|
|
245
|
+
return {
|
|
246
|
+
model: { kind: 'number', integer: false },
|
|
247
|
+
nullable: nullableByType,
|
|
248
|
+
schema: resolved,
|
|
249
|
+
title,
|
|
250
|
+
description,
|
|
251
|
+
};
|
|
252
|
+
case 'integer':
|
|
253
|
+
return {
|
|
254
|
+
model: { kind: 'number', integer: true },
|
|
255
|
+
nullable: nullableByType,
|
|
256
|
+
schema: resolved,
|
|
257
|
+
title,
|
|
258
|
+
description,
|
|
259
|
+
};
|
|
260
|
+
case 'boolean':
|
|
261
|
+
return {
|
|
262
|
+
model: { kind: 'boolean' },
|
|
263
|
+
nullable: nullableByType,
|
|
264
|
+
schema: resolved,
|
|
265
|
+
title,
|
|
266
|
+
description,
|
|
267
|
+
};
|
|
268
|
+
case 'array': {
|
|
269
|
+
if (resolved.items === undefined) {
|
|
270
|
+
return unsupported(resolved, 'array without an items schema', title, description);
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
model: { kind: 'array', items: resolved.items },
|
|
274
|
+
nullable: nullableByType,
|
|
275
|
+
schema: resolved,
|
|
276
|
+
title,
|
|
277
|
+
description,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
case 'object': {
|
|
281
|
+
if (resolved.properties === undefined) {
|
|
282
|
+
return unsupported(
|
|
283
|
+
resolved,
|
|
284
|
+
'free-form object (additionalProperties map) is not supported',
|
|
285
|
+
title,
|
|
286
|
+
description,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
const properties: (readonly [string, JsonSchema])[] = Object.entries(resolved.properties);
|
|
290
|
+
const required = new Set(resolved.required ?? []);
|
|
291
|
+
return {
|
|
292
|
+
model: { kind: 'object', properties, required },
|
|
293
|
+
nullable: nullableByType,
|
|
294
|
+
schema: resolved,
|
|
295
|
+
title,
|
|
296
|
+
description,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
default:
|
|
300
|
+
return unsupported(resolved, `unrecognized type "${String(type)}"`, title, description);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient React contexts for the schema-form tree: the optional argument
|
|
3
|
+
* completion provider and the default media-upload byte cap. A field deep in the
|
|
4
|
+
* tree reads these here rather than every intermediate node drilling the prop.
|
|
5
|
+
*/
|
|
6
|
+
import { createContext } from 'react';
|
|
7
|
+
|
|
8
|
+
import { DEFAULT_MAX_UPLOAD_BYTES } from './media';
|
|
9
|
+
import type { CompletionProvider } from './SchemaForm';
|
|
10
|
+
|
|
11
|
+
// Ambient completion provider for the whole form. A string field deep in the
|
|
12
|
+
// tree reads it here rather than every intermediate node drilling the prop.
|
|
13
|
+
export const CompletionProviderContext = createContext<CompletionProvider | undefined>(undefined);
|
|
14
|
+
|
|
15
|
+
// Ambient default upload cap for the whole form; a media field reads it here so
|
|
16
|
+
// intermediate object/array nodes never drill the prop.
|
|
17
|
+
export const MaxUploadBytesContext = createContext<number>(DEFAULT_MAX_UPLOAD_BYTES);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `defaultValueForSchema` — the initial form value for a schema, honoring
|
|
3
|
+
* `default`/`const` and seeding required nested structure.
|
|
4
|
+
*
|
|
5
|
+
* Inclusion rules keep the seed honest:
|
|
6
|
+
* - a REQUIRED property is seeded (objects → a nested seed, arrays → `[]`); a
|
|
7
|
+
* required scalar with no default is left ABSENT so validation flags it
|
|
8
|
+
* rather than inventing a value;
|
|
9
|
+
* - an OPTIONAL property is included only when it carries an explicit
|
|
10
|
+
* `default` — an optional object/array is NOT auto-materialized, so its own
|
|
11
|
+
* required children can't raise spurious errors on a field the user never
|
|
12
|
+
* opted into.
|
|
13
|
+
*/
|
|
14
|
+
import { classifySchema } from './classify';
|
|
15
|
+
import type { JsonSchema } from './types';
|
|
16
|
+
|
|
17
|
+
/** The explicit `default` (wrapper takes precedence over the resolved node), if any. */
|
|
18
|
+
function explicitDefault(
|
|
19
|
+
raw: JsonSchema,
|
|
20
|
+
resolved: JsonSchema,
|
|
21
|
+
): { present: boolean; value: unknown } {
|
|
22
|
+
if ('default' in raw) return { present: true, value: raw.default };
|
|
23
|
+
if ('default' in resolved) return { present: true, value: resolved.default };
|
|
24
|
+
return { present: false, value: undefined };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The initial value for a schema. `root` carries the `$defs` document for
|
|
29
|
+
* `$ref` resolution and defaults to the schema itself (the common
|
|
30
|
+
* whole-tool-schema call).
|
|
31
|
+
*/
|
|
32
|
+
export function defaultValueForSchema(schema: JsonSchema, root: JsonSchema = schema): unknown {
|
|
33
|
+
const classified = classifySchema(schema, root);
|
|
34
|
+
const explicit = explicitDefault(schema, classified.schema);
|
|
35
|
+
if (explicit.present) return explicit.value;
|
|
36
|
+
|
|
37
|
+
const { model } = classified;
|
|
38
|
+
switch (model.kind) {
|
|
39
|
+
case 'const':
|
|
40
|
+
return model.value;
|
|
41
|
+
case 'object': {
|
|
42
|
+
const result: Record<string, unknown> = {};
|
|
43
|
+
for (const [name, propSchema] of model.properties) {
|
|
44
|
+
if (model.required.has(name)) {
|
|
45
|
+
const seeded = defaultValueForSchema(propSchema, root);
|
|
46
|
+
if (seeded !== undefined) result[name] = seeded;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const propClassified = classifySchema(propSchema, root);
|
|
50
|
+
const propDefault = explicitDefault(propSchema, propClassified.schema);
|
|
51
|
+
if (propDefault.present) result[name] = propDefault.value;
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
case 'array':
|
|
56
|
+
return [];
|
|
57
|
+
case 'enum':
|
|
58
|
+
case 'string':
|
|
59
|
+
case 'number':
|
|
60
|
+
case 'boolean':
|
|
61
|
+
case 'union':
|
|
62
|
+
case 'unsupported':
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|