@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,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useSse` — the live interactions stream, implemented over the api-client's
|
|
3
|
+
* `streamInteractions` (fetch + ReadableStream, never EventSource).
|
|
4
|
+
*
|
|
5
|
+
* SSE event contract: events `interaction.add` /
|
|
6
|
+
* `interaction.answered` / `interaction.removed` / `interaction.backlog_done`.
|
|
7
|
+
* The `add` frame (backlog on connect + live tail) carries the full question;
|
|
8
|
+
* `answered` / `removed` frames carry only ids. On (re)connect the server
|
|
9
|
+
* replays the current pending set as the backlog; this hook treats that backlog
|
|
10
|
+
* as authoritative — an entry not re-seen by `interaction.backlog_done` is
|
|
11
|
+
* dropped, so an interaction removed while the client was disconnected does not
|
|
12
|
+
* linger. `add` frames are at-least-once, so entries are de-duplicated by
|
|
13
|
+
* `interaction_id`, and a redelivered `add` preserves the client `answered` flag.
|
|
14
|
+
*
|
|
15
|
+
* The `Interaction` type is imported type-only (no runtime dep on api-client);
|
|
16
|
+
* frames are validated defensively and a malformed frame is surfaced as an error,
|
|
17
|
+
* never rendered as a silent blank. `answered` is not a wire field — it is a
|
|
18
|
+
* client flag this hook flips on an `interaction.answered` event.
|
|
19
|
+
*/
|
|
20
|
+
import { useEffect, useRef, useState } from 'react';
|
|
21
|
+
import type { Interaction } from '@tai42/api-client';
|
|
22
|
+
|
|
23
|
+
import { useApi } from './useApi';
|
|
24
|
+
import { useOnUnauthorized } from './useUnauthorized';
|
|
25
|
+
|
|
26
|
+
/** A live interaction plus the client-maintained `answered` flag. */
|
|
27
|
+
export type StreamInteraction = Interaction & { readonly answered: boolean };
|
|
28
|
+
|
|
29
|
+
export interface InteractionsStreamState {
|
|
30
|
+
readonly interactions: StreamInteraction[];
|
|
31
|
+
readonly connected: boolean;
|
|
32
|
+
readonly backlogLoaded: boolean;
|
|
33
|
+
readonly error: Error | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Reconnect backoff: capped exponential with full jitter. The delay for attempt
|
|
37
|
+
// n is a random value in [0, min(CAP, BASE * 2**n)); the attempt counter resets
|
|
38
|
+
// only when a connection is proven healthy (`interaction.backlog_done`), not when
|
|
39
|
+
// it merely opens — so a flapping server that accepts the request then drops the
|
|
40
|
+
// body still backs off instead of tight-looping. A healthy tail-reconnect is
|
|
41
|
+
// near-instant while a sustained outage backs off, and the jitter spreads many
|
|
42
|
+
// clients apart rather than retrying in lockstep.
|
|
43
|
+
const RECONNECT_BASE_MS = 1500;
|
|
44
|
+
const RECONNECT_CAP_MS = 30000;
|
|
45
|
+
|
|
46
|
+
// The wire `answer_format` values. Duplicated from the api-client enum by design:
|
|
47
|
+
// the SDK is the leaf and must not import api-client at runtime. The `satisfies`
|
|
48
|
+
// pin against `Interaction['answer_format']` fails to compile if a listed value
|
|
49
|
+
// leaves the union, so this runtime guard cannot silently drift from the type.
|
|
50
|
+
const ANSWER_FORMATS = new Set<string>([
|
|
51
|
+
'text',
|
|
52
|
+
'confirm',
|
|
53
|
+
'select',
|
|
54
|
+
'form',
|
|
55
|
+
'external',
|
|
56
|
+
] as const satisfies readonly Interaction['answer_format'][]);
|
|
57
|
+
|
|
58
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
59
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Parse a frame's JSON to a plain object, or null when it is not one. */
|
|
63
|
+
function parseJson(data: string): Record<string, unknown> | null {
|
|
64
|
+
if (!data) return null;
|
|
65
|
+
try {
|
|
66
|
+
const obj: unknown = JSON.parse(data);
|
|
67
|
+
return isRecord(obj) ? obj : null;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Validate an `interaction.add` frame into an `Interaction`, or null when it is
|
|
75
|
+
* malformed. This mirrors the api-client `interaction` zod schema exactly — it
|
|
76
|
+
* accepts what that schema accepts, no more, no less. The strict-required fields
|
|
77
|
+
* — `interaction_id`, `group_id`, `created_at`, `timeout_at` — must be a string
|
|
78
|
+
* and `answer_format` a known enum value; any missing or non-string one returns
|
|
79
|
+
* null so the caller surfaces it as an error rather than rendering a blank card.
|
|
80
|
+
* Two fields are laxer, matching the schema: `question` is
|
|
81
|
+
* `z.string().default('')`, whose default substitutes ONLY an absent/undefined
|
|
82
|
+
* value, so a missing question normalizes to `''` while a present `null` (or any
|
|
83
|
+
* present non-string) is rejected; `format_payload` is nullish, so an absent/null
|
|
84
|
+
* value (text/confirm carry none) normalizes to `{}` while a present non-object is
|
|
85
|
+
* rejected — never silently coerced to a blank card.
|
|
86
|
+
*/
|
|
87
|
+
function parseAddFrame(data: string): Interaction | null {
|
|
88
|
+
const obj = parseJson(data);
|
|
89
|
+
if (obj === null) return null;
|
|
90
|
+
const {
|
|
91
|
+
interaction_id,
|
|
92
|
+
group_id,
|
|
93
|
+
question,
|
|
94
|
+
answer_format,
|
|
95
|
+
format_payload,
|
|
96
|
+
created_at,
|
|
97
|
+
timeout_at,
|
|
98
|
+
sensitive,
|
|
99
|
+
server_verified,
|
|
100
|
+
channel,
|
|
101
|
+
media,
|
|
102
|
+
} = obj;
|
|
103
|
+
if (typeof interaction_id !== 'string') return null;
|
|
104
|
+
if (typeof group_id !== 'string') return null;
|
|
105
|
+
// `question` is z.string().default('') in the schema: the default substitutes
|
|
106
|
+
// ONLY an absent/undefined value, so a missing question is valid while a
|
|
107
|
+
// present null (or any other present non-string) is malformed.
|
|
108
|
+
if (question !== undefined && typeof question !== 'string') return null;
|
|
109
|
+
if (typeof created_at !== 'string') return null;
|
|
110
|
+
if (typeof timeout_at !== 'string') return null;
|
|
111
|
+
if (typeof answer_format !== 'string' || !ANSWER_FORMATS.has(answer_format)) return null;
|
|
112
|
+
// `format_payload` is nullish in the schema: absent/null normalizes to {}, but
|
|
113
|
+
// a present non-object is malformed and must not be coerced to a blank card.
|
|
114
|
+
if (format_payload != null && !isRecord(format_payload)) return null;
|
|
115
|
+
// `sensitive` is z.boolean().default(false): an absent value normalizes to
|
|
116
|
+
// false, while a present non-boolean is malformed.
|
|
117
|
+
if (sensitive !== undefined && typeof sensitive !== 'boolean') return null;
|
|
118
|
+
// `server_verified` is z.boolean().optional(): an absent value stays undefined,
|
|
119
|
+
// while a present non-boolean is malformed.
|
|
120
|
+
if (server_verified !== undefined && typeof server_verified !== 'boolean') return null;
|
|
121
|
+
// `channel` is z.string().optional(): the delivery-channel name rides the frame
|
|
122
|
+
// only for a channel-delivered question; absent stays undefined, a present
|
|
123
|
+
// non-string is malformed.
|
|
124
|
+
if (channel !== undefined && typeof channel !== 'string') return null;
|
|
125
|
+
// `media` is z.array(z.unknown()).optional(): the display-only media rides the
|
|
126
|
+
// frame only when the question carries some; absent stays undefined, a present
|
|
127
|
+
// non-array is malformed. The ITEMS stay unknown here (loose by design) — each is
|
|
128
|
+
// validated per item by the renderer, so one bad item never fails this frame.
|
|
129
|
+
if (media !== undefined && !Array.isArray(media)) return null;
|
|
130
|
+
return {
|
|
131
|
+
interaction_id,
|
|
132
|
+
group_id,
|
|
133
|
+
question: typeof question === 'string' ? question : '',
|
|
134
|
+
answer_format: answer_format as Interaction['answer_format'],
|
|
135
|
+
format_payload: isRecord(format_payload) ? format_payload : {},
|
|
136
|
+
created_at,
|
|
137
|
+
timeout_at,
|
|
138
|
+
sensitive: sensitive ?? false,
|
|
139
|
+
...(typeof server_verified === 'boolean' ? { server_verified } : {}),
|
|
140
|
+
...(typeof channel === 'string' ? { channel } : {}),
|
|
141
|
+
...(Array.isArray(media) ? { media } : {}),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The interaction id from an `answered` / `removed` frame, or null. */
|
|
146
|
+
function parseId(data: string): string | null {
|
|
147
|
+
const id = parseJson(data)?.interaction_id;
|
|
148
|
+
return typeof id === 'string' ? id : null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function useInteractionsStream(): InteractionsStreamState {
|
|
152
|
+
const api = useApi();
|
|
153
|
+
const onUnauthorized = useOnUnauthorized();
|
|
154
|
+
const [state, setState] = useState<InteractionsStreamState>({
|
|
155
|
+
interactions: [],
|
|
156
|
+
connected: false,
|
|
157
|
+
backlogLoaded: false,
|
|
158
|
+
error: null,
|
|
159
|
+
});
|
|
160
|
+
// Source of truth: a dedupe map keyed by interaction_id, rebuilt into the
|
|
161
|
+
// ordered array on every change.
|
|
162
|
+
const mapRef = useRef<Map<string, StreamInteraction>>(new Map());
|
|
163
|
+
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
const aborted = () => controller.signal.aborted;
|
|
167
|
+
|
|
168
|
+
// A successful data frame rebuilds the list and clears any transient
|
|
169
|
+
// frame-parse error — one bad frame on a healthy stream must not stick.
|
|
170
|
+
const publish = () => {
|
|
171
|
+
setState((prev) => ({
|
|
172
|
+
...prev,
|
|
173
|
+
interactions: Array.from(mapRef.current.values()),
|
|
174
|
+
error: null,
|
|
175
|
+
}));
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const surfaceMalformed = (event: string) => {
|
|
179
|
+
setState((prev) => ({ ...prev, error: new Error(`malformed interaction frame: ${event}`) }));
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Per-connection reconcile state: the replayed backlog is authoritative, so
|
|
183
|
+
// ids not re-seen by `interaction.backlog_done` are dropped as
|
|
184
|
+
// removed-while-disconnected.
|
|
185
|
+
let reconciling = false;
|
|
186
|
+
let seenInBacklog = new Set<string>();
|
|
187
|
+
// Consecutive failed reconnect attempts. Reset only when a connection proves
|
|
188
|
+
// healthy (`interaction.backlog_done`), NOT merely when it opens — a server
|
|
189
|
+
// that accepts the request then drops the body would otherwise reset every
|
|
190
|
+
// time and never back off.
|
|
191
|
+
let reconnectAttempt = 0;
|
|
192
|
+
const beginConnection = () => {
|
|
193
|
+
reconciling = true;
|
|
194
|
+
seenInBacklog = new Set();
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const applyFrame = (event: string, data: string) => {
|
|
198
|
+
if (event === 'interaction.backlog_done') {
|
|
199
|
+
if (reconciling) {
|
|
200
|
+
for (const id of mapRef.current.keys()) {
|
|
201
|
+
if (!seenInBacklog.has(id)) mapRef.current.delete(id);
|
|
202
|
+
}
|
|
203
|
+
reconciling = false;
|
|
204
|
+
}
|
|
205
|
+
// A completed backlog is proof the connection is healthy — clear the
|
|
206
|
+
// reconnect backoff so the next drop reconnects promptly.
|
|
207
|
+
reconnectAttempt = 0;
|
|
208
|
+
setState((prev) => ({
|
|
209
|
+
...prev,
|
|
210
|
+
interactions: Array.from(mapRef.current.values()),
|
|
211
|
+
backlogLoaded: true,
|
|
212
|
+
}));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (event === 'interaction.removed') {
|
|
216
|
+
const id = parseId(data);
|
|
217
|
+
if (id === null) {
|
|
218
|
+
surfaceMalformed(event);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
mapRef.current.delete(id);
|
|
222
|
+
publish();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (event === 'interaction.answered') {
|
|
226
|
+
const id = parseId(data);
|
|
227
|
+
if (id === null) {
|
|
228
|
+
surfaceMalformed(event);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// The answered frame carries only ids — flip the existing item's flag,
|
|
232
|
+
// keeping its question/format. An answered event for an item we never saw
|
|
233
|
+
// has nothing to show.
|
|
234
|
+
const existing = mapRef.current.get(id);
|
|
235
|
+
if (existing) mapRef.current.set(id, { ...existing, answered: true });
|
|
236
|
+
publish();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// interaction.add — the full question.
|
|
240
|
+
const interaction = parseAddFrame(data);
|
|
241
|
+
if (interaction === null) {
|
|
242
|
+
surfaceMalformed(event);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (reconciling) seenInBacklog.add(interaction.interaction_id);
|
|
246
|
+
// Dedupe by id; a redelivered add preserves the client answered flag so a
|
|
247
|
+
// prior `answered` is not undone by an at-least-once replay.
|
|
248
|
+
const existing = mapRef.current.get(interaction.interaction_id);
|
|
249
|
+
mapRef.current.set(interaction.interaction_id, {
|
|
250
|
+
...interaction,
|
|
251
|
+
answered: existing?.answered ?? false,
|
|
252
|
+
});
|
|
253
|
+
publish();
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// The abort signal is the single cancellation source of truth: the cleanup
|
|
257
|
+
// aborts it, and every loop guard reads it fresh.
|
|
258
|
+
const run = async () => {
|
|
259
|
+
while (!aborted()) {
|
|
260
|
+
try {
|
|
261
|
+
const frames = await api.streamInteractions(controller.signal);
|
|
262
|
+
if (aborted()) return;
|
|
263
|
+
beginConnection();
|
|
264
|
+
setState((prev) => ({ ...prev, connected: true, error: null }));
|
|
265
|
+
for await (const frame of frames) {
|
|
266
|
+
if (aborted()) return;
|
|
267
|
+
applyFrame(frame.event, frame.data);
|
|
268
|
+
}
|
|
269
|
+
} catch (err) {
|
|
270
|
+
if (aborted()) return;
|
|
271
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
272
|
+
setState((prev) => ({ ...prev, connected: false, error }));
|
|
273
|
+
if (error.name === 'ApiUnauthorizedError') {
|
|
274
|
+
// A 401 means the stored credential is dead — reconnecting would just
|
|
275
|
+
// replay the same bad key every delay forever. Treat it as terminal
|
|
276
|
+
// and hand off to the app's 401→login handler, the same path every
|
|
277
|
+
// other data call takes; do not loop.
|
|
278
|
+
onUnauthorized();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Stream ended or errored → wait, then reconnect (server replays backlog).
|
|
283
|
+
// Guard before the setState, like every other setState in run(): a
|
|
284
|
+
// microtask scheduled after the `for await` must not write state after
|
|
285
|
+
// the effect cleanup has aborted (and unmounted) the hook.
|
|
286
|
+
if (aborted()) return;
|
|
287
|
+
setState((prev) => ({ ...prev, connected: false }));
|
|
288
|
+
const ceiling = Math.min(RECONNECT_CAP_MS, RECONNECT_BASE_MS * 2 ** reconnectAttempt);
|
|
289
|
+
const delay = Math.random() * ceiling;
|
|
290
|
+
reconnectAttempt += 1;
|
|
291
|
+
await new Promise<void>((resolve) => {
|
|
292
|
+
const onAbort = () => {
|
|
293
|
+
clearTimeout(timer);
|
|
294
|
+
resolve();
|
|
295
|
+
};
|
|
296
|
+
const timer = setTimeout(() => {
|
|
297
|
+
// Normal wake: drop the abort listener so it does not accumulate
|
|
298
|
+
// across reconnects ({ once: true } only self-removes on firing).
|
|
299
|
+
controller.signal.removeEventListener('abort', onAbort);
|
|
300
|
+
resolve();
|
|
301
|
+
}, delay);
|
|
302
|
+
controller.signal.addEventListener('abort', onAbort, { once: true });
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
void run();
|
|
308
|
+
return () => {
|
|
309
|
+
controller.abort();
|
|
310
|
+
};
|
|
311
|
+
}, [api, onUnauthorized]);
|
|
312
|
+
|
|
313
|
+
return state;
|
|
314
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useTheme` — light/dark theme, token-driven. The active theme stamps
|
|
3
|
+
* `data-theme` on the document root, which the DS token layers key off.
|
|
4
|
+
*
|
|
5
|
+
* A three-state PREFERENCE sits behind the resolved theme: `'light'` and
|
|
6
|
+
* `'dark'` pin it, `'system'` follows `prefers-color-scheme` and tracks OS
|
|
7
|
+
* changes live. The resolved `theme` a consumer reads is always the concrete
|
|
8
|
+
* `'light' | 'dark'` the tokens need — never `'system'`.
|
|
9
|
+
*
|
|
10
|
+
* The preference persists to `localStorage`, but ONLY when the operator sets it
|
|
11
|
+
* explicitly (`setPreference` / `setTheme` / `toggle`). Boot READS the stored
|
|
12
|
+
* value; it never WRITES one, so a session that only ever renders the app leaves
|
|
13
|
+
* storage untouched. A `'system'` preference clears any pinned choice and hands
|
|
14
|
+
* the theme back to the OS.
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
createContext,
|
|
18
|
+
createElement,
|
|
19
|
+
useCallback,
|
|
20
|
+
useContext,
|
|
21
|
+
useEffect,
|
|
22
|
+
useMemo,
|
|
23
|
+
useState,
|
|
24
|
+
} from 'react';
|
|
25
|
+
import type { ReactNode } from 'react';
|
|
26
|
+
|
|
27
|
+
export type Theme = 'light' | 'dark';
|
|
28
|
+
export type ThemePreference = 'light' | 'dark' | 'system';
|
|
29
|
+
|
|
30
|
+
export interface ThemeState {
|
|
31
|
+
/** The resolved theme the tokens key off — always concrete, never `'system'`. */
|
|
32
|
+
readonly theme: Theme;
|
|
33
|
+
/** The operator's choice: a pinned theme, or `'system'` to follow the OS. */
|
|
34
|
+
readonly preference: ThemePreference;
|
|
35
|
+
/** Set the preference and persist it. The only path that writes storage. */
|
|
36
|
+
readonly setPreference: (preference: ThemePreference) => void;
|
|
37
|
+
/** Pin an explicit theme (persists) — a preference shortcut kept for callers. */
|
|
38
|
+
readonly setTheme: (theme: Theme) => void;
|
|
39
|
+
/** Flip between the two pinned themes off the current resolved theme (persists). */
|
|
40
|
+
readonly toggle: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ThemeContext = createContext<ThemeState | null>(null);
|
|
44
|
+
|
|
45
|
+
/** Where the explicit preference is stored between sessions. */
|
|
46
|
+
const STORAGE_KEY = 'tai-studio.theme-preference';
|
|
47
|
+
|
|
48
|
+
function systemTheme(): Theme {
|
|
49
|
+
try {
|
|
50
|
+
return globalThis.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
51
|
+
} catch {
|
|
52
|
+
return 'light';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The persisted preference, or `'system'` when none is stored or storage is
|
|
57
|
+
* unavailable. A READ only — boot never writes, so an untouched session leaves
|
|
58
|
+
* storage alone (the login flows depend on this). */
|
|
59
|
+
function readStoredPreference(): ThemePreference {
|
|
60
|
+
try {
|
|
61
|
+
const raw = globalThis.localStorage.getItem(STORAGE_KEY);
|
|
62
|
+
if (raw === 'light' || raw === 'dark' || raw === 'system') return raw;
|
|
63
|
+
} catch {
|
|
64
|
+
// No storage (private mode / non-browser) — the OS preference is the default.
|
|
65
|
+
}
|
|
66
|
+
return 'system';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
70
|
+
const [preference, setPreferenceState] = useState<ThemePreference>(readStoredPreference);
|
|
71
|
+
// The live OS resolution, kept current by the listener below; it only feeds the
|
|
72
|
+
// resolved theme while the preference is `'system'`.
|
|
73
|
+
const [systemResolved, setSystemResolved] = useState<Theme>(systemTheme);
|
|
74
|
+
const theme: Theme = preference === 'system' ? systemResolved : preference;
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
globalThis.document.documentElement.setAttribute('data-theme', theme);
|
|
78
|
+
}, [theme]);
|
|
79
|
+
|
|
80
|
+
// Track the OS preference live, but ONLY while following it. The same defensive
|
|
81
|
+
// guard as `systemTheme()` — jsdom has no `matchMedia`, so subscribing is
|
|
82
|
+
// best-effort and its absence simply leaves the last resolved value in place.
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (preference !== 'system') return;
|
|
85
|
+
let query: MediaQueryList;
|
|
86
|
+
try {
|
|
87
|
+
query = globalThis.matchMedia('(prefers-color-scheme: dark)');
|
|
88
|
+
} catch {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
setSystemResolved(query.matches ? 'dark' : 'light');
|
|
92
|
+
const onChange = (event: MediaQueryListEvent): void => {
|
|
93
|
+
setSystemResolved(event.matches ? 'dark' : 'light');
|
|
94
|
+
};
|
|
95
|
+
query.addEventListener('change', onChange);
|
|
96
|
+
return () => {
|
|
97
|
+
query.removeEventListener('change', onChange);
|
|
98
|
+
};
|
|
99
|
+
}, [preference]);
|
|
100
|
+
|
|
101
|
+
const setPreference = useCallback((next: ThemePreference) => {
|
|
102
|
+
setPreferenceState(next);
|
|
103
|
+
try {
|
|
104
|
+
globalThis.localStorage.setItem(STORAGE_KEY, next);
|
|
105
|
+
} catch {
|
|
106
|
+
// Storage unavailable — the choice still applies for this session in memory.
|
|
107
|
+
}
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
110
|
+
const setTheme = useCallback(
|
|
111
|
+
(next: Theme) => {
|
|
112
|
+
setPreference(next);
|
|
113
|
+
},
|
|
114
|
+
[setPreference],
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const toggle = useCallback(() => {
|
|
118
|
+
setPreference(theme === 'dark' ? 'light' : 'dark');
|
|
119
|
+
}, [setPreference, theme]);
|
|
120
|
+
|
|
121
|
+
const value = useMemo<ThemeState>(
|
|
122
|
+
() => ({ theme, preference, setPreference, setTheme, toggle }),
|
|
123
|
+
[theme, preference, setPreference, setTheme, toggle],
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
return createElement(ThemeContext.Provider, { value }, children);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function useTheme(): ThemeState {
|
|
130
|
+
const state = useContext(ThemeContext);
|
|
131
|
+
if (state === null) throw new Error('useTheme must be used within a <ThemeProvider>');
|
|
132
|
+
return state;
|
|
133
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useOnUnauthorized` — the app's "the credential is dead, route to login" handler,
|
|
3
|
+
* provided through an SDK-owned context so shared hooks can reach it without a
|
|
4
|
+
* runtime dependency on the shell or the router.
|
|
5
|
+
*
|
|
6
|
+
* The default is a no-op, so a consumer rendered without a provider (a test, or a
|
|
7
|
+
* plugin in isolation) still works — it just does not route. The shell wires the
|
|
8
|
+
* real handler (the same one the api-client's 401 path drives) so a 401 raised
|
|
9
|
+
* OUTSIDE TanStack Query — notably the raw interactions SSE fetch — lands on the
|
|
10
|
+
* one login-routing path instead of being retried against a known-bad key.
|
|
11
|
+
*/
|
|
12
|
+
import { createContext, useContext } from 'react';
|
|
13
|
+
|
|
14
|
+
const UnauthorizedContext = createContext<() => void>(() => undefined);
|
|
15
|
+
|
|
16
|
+
export const UnauthorizedProvider = UnauthorizedContext.Provider;
|
|
17
|
+
|
|
18
|
+
export function useOnUnauthorized(): () => void {
|
|
19
|
+
return useContext(UnauthorizedContext);
|
|
20
|
+
}
|
package/src/host.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tai42/studio-sdk/host — the HOST-ONLY registry API. `loadPlugin` (the one entry
|
|
3
|
+
* through which a plugin's `register` contributes) and `getContributions` (what the
|
|
4
|
+
* shell reads) live here, apart from the plugin surface, so the served plugin asset
|
|
5
|
+
* carries no way to forge, wipe, or enumerate the registry. Only the host bundle
|
|
6
|
+
* imports this. See SECURITY.md for the trust boundary.
|
|
7
|
+
*/
|
|
8
|
+
export { loadPlugin, getContributions } from './plugin/registry';
|
|
9
|
+
export {
|
|
10
|
+
setPluginHostState,
|
|
11
|
+
getPluginHostState,
|
|
12
|
+
subscribePluginHost,
|
|
13
|
+
usePluginContributions,
|
|
14
|
+
} from './plugin/host-state';
|
|
15
|
+
export type { PluginLoaderState, PluginContributionsSnapshot } from './plugin/host-state';
|