@tai42/studio-sdk 16.0.0 → 16.0.1
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/dist/components/explorer-body.d.ts +49 -0
- package/dist/components/explorer-body.d.ts.map +1 -0
- package/dist/components/explorer-body.js +67 -0
- package/dist/components/explorer-body.js.map +1 -0
- package/dist/components/explorer-page-size.d.ts +4 -0
- package/dist/components/explorer-page-size.d.ts.map +1 -0
- package/dist/components/explorer-page-size.js +36 -0
- package/dist/components/explorer-page-size.js.map +1 -0
- package/dist/components/explorer-tag-filter.d.ts +17 -0
- package/dist/components/explorer-tag-filter.d.ts.map +1 -0
- package/dist/components/explorer-tag-filter.js +29 -0
- package/dist/components/explorer-tag-filter.js.map +1 -0
- package/dist/components/explorer-tags.d.ts +22 -0
- package/dist/components/explorer-tags.d.ts.map +1 -0
- package/dist/components/explorer-tags.js +51 -0
- package/dist/components/explorer-tags.js.map +1 -0
- package/dist/components/explorer-view.d.ts +3 -17
- package/dist/components/explorer-view.d.ts.map +1 -1
- package/dist/components/explorer-view.js +53 -170
- package/dist/components/explorer-view.js.map +1 -1
- package/dist/components/extension-combo-builder.d.ts +0 -5
- package/dist/components/extension-combo-builder.d.ts.map +1 -1
- package/dist/components/extension-combo-builder.js +4 -42
- package/dist/components/extension-combo-builder.js.map +1 -1
- package/dist/components/extension-combo-parts.d.ts +44 -0
- package/dist/components/extension-combo-parts.d.ts.map +1 -0
- package/dist/components/extension-combo-parts.js +57 -0
- package/dist/components/extension-combo-parts.js.map +1 -0
- package/dist/components/fleet-report.d.ts.map +1 -1
- package/dist/components/fleet-report.js +51 -37
- package/dist/components/fleet-report.js.map +1 -1
- package/dist/components/icons/chrome-marks.d.ts +50 -0
- package/dist/components/icons/chrome-marks.d.ts.map +1 -0
- package/dist/components/icons/chrome-marks.js +51 -0
- package/dist/components/icons/chrome-marks.js.map +1 -0
- package/dist/components/icons/icon-frame.d.ts +29 -0
- package/dist/components/icons/icon-frame.d.ts.map +1 -0
- package/dist/components/icons/icon-frame.js +19 -0
- package/dist/components/icons/icon-frame.js.map +1 -0
- package/dist/components/icons/index.d.ts +40 -0
- package/dist/components/icons/index.d.ts.map +1 -0
- package/dist/components/icons/index.js +6 -0
- package/dist/components/icons/index.js.map +1 -0
- package/dist/components/icons/nav-map.d.ts +14 -0
- package/dist/components/icons/nav-map.d.ts.map +1 -0
- package/dist/components/icons/nav-map.js +28 -0
- package/dist/components/icons/nav-map.js.map +1 -0
- package/dist/components/icons/nav-marks.d.ts +44 -0
- package/dist/components/icons/nav-marks.d.ts.map +1 -0
- package/dist/components/icons/nav-marks.js +45 -0
- package/dist/components/icons/nav-marks.js.map +1 -0
- package/dist/components/icons/status-marks.d.ts +21 -0
- package/dist/components/icons/status-marks.d.ts.map +1 -0
- package/dist/components/icons/status-marks.js +22 -0
- package/dist/components/icons/status-marks.js.map +1 -0
- package/dist/components/icons/theme-marks.d.ts +9 -0
- package/dist/components/icons/theme-marks.d.ts.map +1 -0
- package/dist/components/icons/theme-marks.js +10 -0
- package/dist/components/icons/theme-marks.js.map +1 -0
- package/dist/components/json-diff.d.ts.map +1 -1
- package/dist/components/json-diff.js +1 -3
- package/dist/components/json-diff.js.map +1 -1
- package/dist/components/json-tree-context.d.ts +13 -0
- package/dist/components/json-tree-context.d.ts.map +1 -0
- package/dist/components/json-tree-context.js +15 -0
- package/dist/components/json-tree-context.js.map +1 -0
- package/dist/components/json-tree-model.d.ts +56 -0
- package/dist/components/json-tree-model.d.ts.map +1 -0
- package/dist/components/json-tree-model.js +125 -0
- package/dist/components/json-tree-model.js.map +1 -0
- package/dist/components/json-tree-nodes.d.ts +30 -0
- package/dist/components/json-tree-nodes.d.ts.map +1 -0
- package/dist/components/json-tree-nodes.js +118 -0
- package/dist/components/json-tree-nodes.js.map +1 -0
- package/dist/components/json-tree.d.ts.map +1 -1
- package/dist/components/json-tree.js +7 -295
- package/dist/components/json-tree.js.map +1 -1
- package/dist/components/json-value-format.d.ts +20 -0
- package/dist/components/json-value-format.d.ts.map +1 -0
- package/dist/components/json-value-format.js +63 -0
- package/dist/components/json-value-format.js.map +1 -0
- package/dist/components/markdown-blocks.d.ts +38 -0
- package/dist/components/markdown-blocks.d.ts.map +1 -0
- package/dist/components/markdown-blocks.js +168 -0
- package/dist/components/markdown-blocks.js.map +1 -0
- package/dist/components/markdown-inline.d.ts +24 -0
- package/dist/components/markdown-inline.d.ts.map +1 -0
- package/dist/components/markdown-inline.js +257 -0
- package/dist/components/markdown-inline.js.map +1 -0
- package/dist/components/markdown.d.ts +3 -52
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +4 -376
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/observer-registry.d.ts +38 -0
- package/dist/components/observer-registry.d.ts.map +1 -0
- package/dist/components/observer-registry.js +131 -0
- package/dist/components/observer-registry.js.map +1 -0
- package/dist/components/overflow-measure.d.ts +38 -0
- package/dist/components/overflow-measure.d.ts.map +1 -0
- package/dist/components/overflow-measure.js +54 -0
- package/dist/components/overflow-measure.js.map +1 -0
- package/dist/components/prose-regions.d.ts +81 -0
- package/dist/components/prose-regions.d.ts.map +1 -0
- package/dist/components/prose-regions.js +128 -0
- package/dist/components/prose-regions.js.map +1 -0
- package/dist/components/scroll-region.d.ts.map +1 -1
- package/dist/components/scroll-region.js +3 -298
- package/dist/components/scroll-region.js.map +1 -1
- package/dist/components/tab-stop-parse.d.ts +53 -0
- package/dist/components/tab-stop-parse.d.ts.map +1 -0
- package/dist/components/tab-stop-parse.js +202 -0
- package/dist/components/tab-stop-parse.js.map +1 -0
- package/dist/components/templated-text-field.d.ts.map +1 -1
- package/dist/components/templated-text-field.js +10 -148
- package/dist/components/templated-text-field.js.map +1 -1
- package/dist/components/templated-text-kwargs-editor.d.ts +24 -0
- package/dist/components/templated-text-kwargs-editor.d.ts.map +1 -0
- package/dist/components/templated-text-kwargs-editor.js +48 -0
- package/dist/components/templated-text-kwargs-editor.js.map +1 -0
- package/dist/components/templated-text-kwargs.d.ts +37 -0
- package/dist/components/templated-text-kwargs.d.ts.map +1 -0
- package/dist/components/templated-text-kwargs.js +66 -0
- package/dist/components/templated-text-kwargs.js.map +1 -0
- package/dist/components/templated-text-source.d.ts +33 -0
- package/dist/components/templated-text-source.d.ts.map +1 -0
- package/dist/components/templated-text-source.js +53 -0
- package/dist/components/templated-text-source.js.map +1 -0
- package/dist/components/tool-picker-parts.d.ts +39 -0
- package/dist/components/tool-picker-parts.d.ts.map +1 -0
- package/dist/components/tool-picker-parts.js +87 -0
- package/dist/components/tool-picker-parts.js.map +1 -0
- package/dist/components/tool-picker.d.ts +1 -2
- package/dist/components/tool-picker.d.ts.map +1 -1
- package/dist/components/tool-picker.js +4 -66
- package/dist/components/tool-picker.js.map +1 -1
- package/dist/components/version-history-panel.d.ts.map +1 -1
- package/dist/components/version-history-panel.js +7 -34
- package/dist/components/version-history-panel.js.map +1 -1
- package/dist/components/version-history-rows.d.ts +48 -0
- package/dist/components/version-history-rows.d.ts.map +1 -0
- package/dist/components/version-history-rows.js +51 -0
- package/dist/components/version-history-rows.js.map +1 -0
- package/dist/guards.d.ts +6 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/guards.js +8 -0
- package/dist/guards.js.map +1 -0
- package/dist/hooks/interactions-connection.d.ts +43 -0
- package/dist/hooks/interactions-connection.d.ts.map +1 -0
- package/dist/hooks/interactions-connection.js +171 -0
- package/dist/hooks/interactions-connection.js.map +1 -0
- package/dist/hooks/interactions-stream.d.ts +80 -0
- package/dist/hooks/interactions-stream.d.ts.map +1 -0
- package/dist/hooks/interactions-stream.js +233 -0
- package/dist/hooks/interactions-stream.js.map +1 -0
- package/dist/hooks/useApi.d.ts +2192 -2192
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/hooks/useSse.d.ts +3 -6
- package/dist/hooks/useSse.d.ts.map +1 -1
- package/dist/hooks/useSse.js +38 -451
- package/dist/hooks/useSse.js.map +1 -1
- package/dist/navigation/app-link.d.ts +25 -0
- package/dist/navigation/app-link.d.ts.map +1 -0
- package/dist/navigation/app-link.js +33 -0
- package/dist/navigation/app-link.js.map +1 -0
- package/dist/navigation/context.d.ts +3 -20
- package/dist/navigation/context.d.ts.map +1 -1
- package/dist/navigation/context.js +7 -267
- package/dist/navigation/context.js.map +1 -1
- package/dist/navigation/entry-state.d.ts +23 -0
- package/dist/navigation/entry-state.d.ts.map +1 -0
- package/dist/navigation/entry-state.js +39 -0
- package/dist/navigation/entry-state.js.map +1 -0
- package/dist/navigation/guard-registry.d.ts +30 -0
- package/dist/navigation/guard-registry.d.ts.map +1 -0
- package/dist/navigation/guard-registry.js +46 -0
- package/dist/navigation/guard-registry.js.map +1 -0
- package/dist/navigation/guarded-navigation.d.ts +24 -0
- package/dist/navigation/guarded-navigation.d.ts.map +1 -0
- package/dist/navigation/guarded-navigation.js +102 -0
- package/dist/navigation/guarded-navigation.js.map +1 -0
- package/dist/navigation/index.d.ts +3 -2
- package/dist/navigation/index.d.ts.map +1 -1
- package/dist/navigation/index.js +2 -1
- package/dist/navigation/index.js.map +1 -1
- package/dist/navigation/types.d.ts +8 -11
- package/dist/navigation/types.d.ts.map +1 -1
- package/dist/navigation/use-back-forward-guard.d.ts +5 -0
- package/dist/navigation/use-back-forward-guard.d.ts.map +1 -0
- package/dist/navigation/use-back-forward-guard.js +82 -0
- package/dist/navigation/use-back-forward-guard.js.map +1 -0
- package/dist/plugin/registry.d.ts +6 -15
- package/dist/plugin/registry.d.ts.map +1 -1
- package/dist/plugin/registry.js +83 -75
- package/dist/plugin/registry.js.map +1 -1
- package/dist/schema-form/SecretRefField.d.ts +2 -3
- package/dist/schema-form/SecretRefField.d.ts.map +1 -1
- package/dist/schema-form/SecretRefField.js +23 -12
- package/dist/schema-form/SecretRefField.js.map +1 -1
- package/dist/schema-form/classify.d.ts +1 -122
- package/dist/schema-form/classify.d.ts.map +1 -1
- package/dist/schema-form/classify.js +174 -237
- package/dist/schema-form/classify.js.map +1 -1
- package/dist/schema-form/default-value.d.ts.map +1 -1
- package/dist/schema-form/default-value.js +42 -19
- package/dist/schema-form/default-value.js.map +1 -1
- package/dist/schema-form/enum-field.d.ts +1 -1
- package/dist/schema-form/enum-field.d.ts.map +1 -1
- package/dist/schema-form/field-model.d.ts +121 -0
- package/dist/schema-form/field-model.d.ts.map +1 -0
- package/dist/schema-form/field-model.js +2 -0
- package/dist/schema-form/field-model.js.map +1 -0
- package/dist/schema-form/field-node.d.ts.map +1 -1
- package/dist/schema-form/field-node.js +25 -14
- package/dist/schema-form/field-node.js.map +1 -1
- package/dist/schema-form/media-field.d.ts +1 -1
- package/dist/schema-form/media-field.d.ts.map +1 -1
- package/dist/schema-form/media-field.js +47 -28
- package/dist/schema-form/media-field.js.map +1 -1
- package/dist/schema-form/string-annotations.d.ts +20 -0
- package/dist/schema-form/string-annotations.d.ts.map +1 -0
- package/dist/schema-form/string-annotations.js +105 -0
- package/dist/schema-form/string-annotations.js.map +1 -0
- package/dist/schema-form/string-field.d.ts +1 -1
- package/dist/schema-form/string-field.d.ts.map +1 -1
- package/dist/schema-form/union-field.d.ts +1 -1
- package/dist/schema-form/union-field.d.ts.map +1 -1
- package/dist/schema-form/union-field.js.map +1 -1
- package/dist/schema-form/union.d.ts +1 -1
- package/dist/schema-form/union.d.ts.map +1 -1
- package/dist/schema-form/validate.d.ts.map +1 -1
- package/dist/schema-form/validate.js +126 -105
- package/dist/schema-form/validate.js.map +1 -1
- package/dist/state-binding/AdapterMapping.d.ts.map +1 -1
- package/dist/state-binding/AdapterMapping.js +0 -0
- package/dist/state-binding/AdapterMapping.js.map +1 -1
- package/dist/state-binding/StateAttachRow.d.ts.map +1 -1
- package/dist/state-binding/StateAttachRow.js +52 -36
- package/dist/state-binding/StateAttachRow.js.map +1 -1
- package/dist/state-binding/adapter-mapping-row.d.ts +17 -0
- package/dist/state-binding/adapter-mapping-row.d.ts.map +1 -0
- package/dist/state-binding/adapter-mapping-row.js +87 -0
- package/dist/state-binding/adapter-mapping-row.js.map +1 -0
- package/dist/structured-output/StructuredOutput.d.ts.map +1 -1
- package/dist/structured-output/StructuredOutput.js +1 -3
- package/dist/structured-output/StructuredOutput.js.map +1 -1
- package/package.json +2 -2
- package/src/components/explorer-body.tsx +239 -0
- package/src/components/explorer-page-size.ts +39 -0
- package/src/components/explorer-tag-filter.tsx +80 -0
- package/src/components/explorer-tags.ts +64 -0
- package/src/components/explorer-view.tsx +114 -366
- package/src/components/extension-combo-builder.tsx +35 -152
- package/src/components/extension-combo-parts.tsx +237 -0
- package/src/components/fleet-report.tsx +97 -54
- package/src/components/icons/chrome-marks.tsx +195 -0
- package/src/components/icons/icon-frame.tsx +50 -0
- package/src/components/icons/index.ts +84 -0
- package/src/components/icons/nav-map.ts +55 -0
- package/src/components/icons/nav-marks.tsx +205 -0
- package/src/components/icons/status-marks.tsx +46 -0
- package/src/components/icons/theme-marks.tsx +33 -0
- package/src/components/json-diff.tsx +1 -4
- package/src/components/json-tree-context.ts +26 -0
- package/src/components/json-tree-model.ts +139 -0
- package/src/components/json-tree-nodes.tsx +224 -0
- package/src/components/json-tree.tsx +15 -434
- package/src/components/json-value-format.ts +64 -0
- package/src/components/markdown-blocks.ts +198 -0
- package/src/components/markdown-inline.tsx +365 -0
- package/src/components/markdown.tsx +5 -471
- package/src/components/observer-registry.ts +139 -0
- package/src/components/overflow-measure.ts +69 -0
- package/src/components/prose-regions.ts +153 -0
- package/src/components/scroll-region.tsx +20 -360
- package/src/components/tab-stop-parse.ts +217 -0
- package/src/components/templated-text-field.tsx +38 -330
- package/src/components/templated-text-kwargs-editor.tsx +147 -0
- package/src/components/templated-text-kwargs.ts +92 -0
- package/src/components/templated-text-source.tsx +226 -0
- package/src/components/tool-picker-parts.tsx +184 -0
- package/src/components/tool-picker.tsx +22 -114
- package/src/components/version-history-panel.tsx +38 -142
- package/src/components/version-history-rows.tsx +220 -0
- package/src/guards.ts +7 -0
- package/src/hooks/interactions-connection.ts +227 -0
- package/src/hooks/interactions-stream.ts +274 -0
- package/src/hooks/useSse.ts +50 -477
- package/src/navigation/app-link.tsx +70 -0
- package/src/navigation/context.tsx +11 -330
- package/src/navigation/entry-state.ts +42 -0
- package/src/navigation/guard-registry.ts +65 -0
- package/src/navigation/guarded-navigation.ts +147 -0
- package/src/navigation/index.ts +2 -2
- package/src/navigation/types.ts +8 -11
- package/src/navigation/use-back-forward-guard.ts +90 -0
- package/src/plugin/registry.ts +111 -90
- package/src/schema-form/SecretRefField.tsx +121 -71
- package/src/schema-form/classify.ts +244 -395
- package/src/schema-form/default-value.ts +48 -17
- package/src/schema-form/enum-field.tsx +1 -1
- package/src/schema-form/field-model.ts +133 -0
- package/src/schema-form/field-node.tsx +94 -38
- package/src/schema-form/media-field.tsx +134 -65
- package/src/schema-form/string-annotations.ts +111 -0
- package/src/schema-form/string-field.tsx +1 -1
- package/src/schema-form/union-field.tsx +2 -1
- package/src/schema-form/union.ts +1 -1
- package/src/schema-form/validate.ts +184 -103
- package/src/state-binding/AdapterMapping.tsx +0 -0
- package/src/state-binding/StateAttachRow.tsx +222 -147
- package/src/state-binding/adapter-mapping-row.tsx +232 -0
- package/src/structured-output/StructuredOutput.tsx +1 -4
- package/dist/components/icons.d.ts +0 -160
- package/dist/components/icons.d.ts.map +0 -1
- package/dist/components/icons.js +0 -160
- package/dist/components/icons.js.map +0 -1
- package/src/components/icons.tsx +0 -586
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser back/forward and full-unload arm of the navigation guard. While any
|
|
3
|
+
* guard is armed it intercepts `popstate` — canceling the move by re-pushing the
|
|
4
|
+
* committed entry, consulting the guards once, and replaying the move on approval —
|
|
5
|
+
* and installs a `beforeunload` prompt. It attaches the listeners only while armed,
|
|
6
|
+
* mirroring the registry's armed state into React.
|
|
7
|
+
*/
|
|
8
|
+
import { useEffect, useState } from 'react';
|
|
9
|
+
import type { RefObject } from 'react';
|
|
10
|
+
|
|
11
|
+
import type { NavigationGuardRegistry } from './guard-registry';
|
|
12
|
+
import type { CommittedEntry } from './guarded-navigation';
|
|
13
|
+
|
|
14
|
+
export function useBackForwardGuard(
|
|
15
|
+
registry: NavigationGuardRegistry,
|
|
16
|
+
committedRef: RefObject<CommittedEntry | null>,
|
|
17
|
+
): void {
|
|
18
|
+
// Mirror the registry's armed state into React so the browser listeners attach only
|
|
19
|
+
// while at least one guard is armed.
|
|
20
|
+
const [armed, setArmed] = useState(false);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
setArmed(registry.hasArmedGuards());
|
|
23
|
+
return registry.subscribe(() => {
|
|
24
|
+
setArmed(registry.hasArmedGuards());
|
|
25
|
+
});
|
|
26
|
+
}, [registry]);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!armed) return;
|
|
30
|
+
|
|
31
|
+
// The protected entry (URL + state); a back/forward landing elsewhere is first
|
|
32
|
+
// canceled back to here while the guard is consulted.
|
|
33
|
+
committedRef.current = { href: window.location.href, state: window.history.state };
|
|
34
|
+
// Set while replaying a guard-approved back/forward, so its `popstate` passes
|
|
35
|
+
// through instead of being re-intercepted.
|
|
36
|
+
let bypass = false;
|
|
37
|
+
// Latched while a decision is pending, so a second Back mid-decision keeps canceling
|
|
38
|
+
// without starting a concurrent `registry.run()` — one gesture, one dialog.
|
|
39
|
+
let pending = false;
|
|
40
|
+
// Restore pushes stacked for the pending decision, so an approved replay unwinds
|
|
41
|
+
// exactly that far in one `go(-depth)`. Reset per decision.
|
|
42
|
+
let restores = 0;
|
|
43
|
+
|
|
44
|
+
const onPopState = () => {
|
|
45
|
+
if (bypass) {
|
|
46
|
+
bypass = false;
|
|
47
|
+
committedRef.current = { href: window.location.href, state: window.history.state };
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!registry.hasArmedGuards()) {
|
|
51
|
+
committedRef.current = { href: window.location.href, state: window.history.state };
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const committed = committedRef.current;
|
|
55
|
+
if (committed === null) return;
|
|
56
|
+
// Cancel the browser's move by pushing the committed entry on top of wherever it
|
|
57
|
+
// landed. popstate exposes no direction/distance and the router owns
|
|
58
|
+
// `history.state`, so restore can only push, not delete. A vetoed forward thus
|
|
59
|
+
// strands a stub entry — an accepted limit the next pushState truncates.
|
|
60
|
+
window.history.pushState(committed.state, '', committed.href);
|
|
61
|
+
restores += 1;
|
|
62
|
+
if (pending) return;
|
|
63
|
+
pending = true;
|
|
64
|
+
void registry.run().then((allowed) => {
|
|
65
|
+
pending = false;
|
|
66
|
+
const depth = restores;
|
|
67
|
+
restores = 0;
|
|
68
|
+
if (!allowed) return;
|
|
69
|
+
bypass = true;
|
|
70
|
+
window.history.go(-depth);
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const onBeforeUnload = (event: BeforeUnloadEvent) => {
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
// Browsers below the floor (Chrome 108) gate the prompt on a set `returnValue`,
|
|
77
|
+
// not `preventDefault` alone; the string is ignored. Typed deprecated, still required.
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated -- required beforeunload trigger for the supported browser range.
|
|
79
|
+
event.returnValue = '';
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
window.addEventListener('popstate', onPopState);
|
|
83
|
+
window.addEventListener('beforeunload', onBeforeUnload);
|
|
84
|
+
return () => {
|
|
85
|
+
committedRef.current = null;
|
|
86
|
+
window.removeEventListener('popstate', onPopState);
|
|
87
|
+
window.removeEventListener('beforeunload', onBeforeUnload);
|
|
88
|
+
};
|
|
89
|
+
}, [armed, registry, committedRef]);
|
|
90
|
+
}
|
package/src/plugin/registry.ts
CHANGED
|
@@ -24,117 +24,115 @@ const pages: RegisteredPage[] = [];
|
|
|
24
24
|
const settingsTabs: RegisteredSettingsTab[] = [];
|
|
25
25
|
const navEntries: RegisteredNavEntry[] = [];
|
|
26
26
|
|
|
27
|
+
/** The contributions one plugin stages before they commit as a batch. */
|
|
28
|
+
interface StagedContributions {
|
|
29
|
+
readonly pages: RegisteredPage[];
|
|
30
|
+
readonly toolPanels: Map<string, ToolPanelContribution>;
|
|
31
|
+
readonly settingsTabs: RegisteredSettingsTab[];
|
|
32
|
+
readonly tabIds: Set<string>;
|
|
33
|
+
readonly navEntries: RegisteredNavEntry[];
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* Contributions are STAGED into local arrays (identity captured in the closure,
|
|
32
|
-
* never read from ambient state) and only committed after `entry` settles
|
|
33
|
-
* successfully. `entry` is AWAITED before commit, so a synchronous or an `async`
|
|
34
|
-
* entry is fully done first: a post-`await` throw skips the commit (atomicity
|
|
35
|
-
* holds for async too) and a post-`await` registration cannot slip past the batch.
|
|
36
|
-
* The seal is set after `entry` has SETTLED — for a sync entry, after it returns
|
|
37
|
-
* and the `await` continuation runs; for an async entry, after its returned promise
|
|
38
|
-
* resolves. A registration attempted from that point on (a deferred timer, a
|
|
39
|
-
* callback scheduled after settle) throws loudly instead of pushing into an
|
|
40
|
-
* orphaned staged array that never commits. Registrations made during `entry`'s
|
|
41
|
-
* synchronous body — including a microtask it enqueues then, which runs before the
|
|
42
|
-
* `await` continuation — are still captured and committed with the batch. A
|
|
43
|
-
* `register` that throws commits nothing and leaves the registry untouched.
|
|
44
|
-
* Duplicate guards run against BOTH the staged contributions and what is already
|
|
45
|
-
* committed, and fail loudly.
|
|
37
|
+
* A mutable seal flag passed BY REFERENCE into the staging context, so the seal set
|
|
38
|
+
* in {@link loadPlugin}'s `finally` is seen by every `register*` method's closure.
|
|
46
39
|
*/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const stagedSettingsTabs: RegisteredSettingsTab[] = [];
|
|
51
|
-
const stagedTabIds = new Set<string>();
|
|
52
|
-
const stagedNavEntries: RegisteredNavEntry[] = [];
|
|
53
|
-
let sealed = false;
|
|
40
|
+
interface Seal {
|
|
41
|
+
closed: boolean;
|
|
42
|
+
}
|
|
54
43
|
|
|
55
|
-
|
|
44
|
+
function emptyStaged(): StagedContributions {
|
|
45
|
+
return {
|
|
46
|
+
pages: [],
|
|
47
|
+
toolPanels: new Map(),
|
|
48
|
+
settingsTabs: [],
|
|
49
|
+
tabIds: new Set(),
|
|
50
|
+
navEntries: [],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build the {@link PluginContext} a plugin's `register` entry contributes through.
|
|
56
|
+
* Every method stages into `staged` (never ambient state) and refuses once `seal` is
|
|
57
|
+
* closed — a registration after `entry` settles (a deferred timer, a post-settle
|
|
58
|
+
* callback) throws loudly rather than pushing into an orphaned array that never
|
|
59
|
+
* commits. Duplicate guards run against BOTH the staged set and what is already
|
|
60
|
+
* committed, and fail loudly.
|
|
61
|
+
*/
|
|
62
|
+
function makeStagingContext(
|
|
63
|
+
pluginId: string,
|
|
64
|
+
staged: StagedContributions,
|
|
65
|
+
seal: Seal,
|
|
66
|
+
): PluginContext {
|
|
67
|
+
const assertOpen = (): void => {
|
|
68
|
+
if (seal.closed) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
'registration is closed: a plugin must register during register(), not after it resolves',
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
56
75
|
registerPage(contribution: PageContribution): void {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
const staged = stagedPages.some((p) => p.path === contribution.path);
|
|
63
|
-
const committed = pages.some((p) => p.pluginId === pluginId && p.path === contribution.path);
|
|
64
|
-
if (staged || committed) {
|
|
76
|
+
assertOpen();
|
|
77
|
+
const duplicate =
|
|
78
|
+
staged.pages.some((p) => p.path === contribution.path) ||
|
|
79
|
+
pages.some((p) => p.pluginId === pluginId && p.path === contribution.path);
|
|
80
|
+
if (duplicate) {
|
|
65
81
|
// A duplicate path within one plugin is an author mistake that would
|
|
66
82
|
// leave one page unreachable — fail loudly, as registerToolPanel does.
|
|
67
83
|
throw new Error(`plugin “${pluginId}” already registered a page at “${contribution.path}”`);
|
|
68
84
|
}
|
|
69
|
-
|
|
85
|
+
staged.pages.push({ ...contribution, pluginId });
|
|
70
86
|
},
|
|
71
87
|
registerToolPanel(contribution: ToolPanelContribution): void {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
'registration is closed: a plugin must register during register(), not after it resolves',
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
if (stagedToolPanels.has(contribution.toolName) || toolPanels.has(contribution.toolName)) {
|
|
88
|
+
assertOpen();
|
|
89
|
+
if (staged.toolPanels.has(contribution.toolName) || toolPanels.has(contribution.toolName)) {
|
|
78
90
|
throw new Error(`a tool panel is already registered for ${contribution.toolName}`);
|
|
79
91
|
}
|
|
80
|
-
|
|
92
|
+
staged.toolPanels.set(contribution.toolName, contribution);
|
|
81
93
|
},
|
|
82
94
|
registerSettingsTab(contribution: SettingsTabContribution): void {
|
|
83
|
-
|
|
84
|
-
throw new Error(
|
|
85
|
-
'registration is closed: a plugin must register during register(), not after it resolves',
|
|
86
|
-
);
|
|
87
|
-
}
|
|
95
|
+
assertOpen();
|
|
88
96
|
const committed = settingsTabs.some(
|
|
89
97
|
(t) => t.pluginId === pluginId && t.id === contribution.id,
|
|
90
98
|
);
|
|
91
|
-
if (
|
|
99
|
+
if (staged.tabIds.has(contribution.id) || committed) {
|
|
92
100
|
// A duplicate tab id within one plugin would leave one tab unreachable —
|
|
93
101
|
// fail loudly, mirroring the page-path guard.
|
|
94
102
|
throw new Error(
|
|
95
103
|
`plugin “${pluginId}” already registered a settings tab “${contribution.id}”`,
|
|
96
104
|
);
|
|
97
105
|
}
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
staged.tabIds.add(contribution.id);
|
|
107
|
+
staged.settingsTabs.push({ ...contribution, pluginId });
|
|
100
108
|
},
|
|
101
109
|
registerNavEntry(contribution: NavEntryContribution): void {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
const staged = stagedNavEntries.some((e) => e.path === contribution.path);
|
|
108
|
-
const committed = navEntries.some(
|
|
109
|
-
(e) => e.pluginId === pluginId && e.path === contribution.path,
|
|
110
|
-
);
|
|
111
|
-
if (staged || committed) {
|
|
110
|
+
assertOpen();
|
|
111
|
+
const duplicate =
|
|
112
|
+
staged.navEntries.some((e) => e.path === contribution.path) ||
|
|
113
|
+
navEntries.some((e) => e.pluginId === pluginId && e.path === contribution.path);
|
|
114
|
+
if (duplicate) {
|
|
112
115
|
// A duplicate nav path within one plugin is an author mistake — two
|
|
113
116
|
// entries pointing at the same page — so fail loudly like the page guard.
|
|
114
117
|
throw new Error(
|
|
115
118
|
`plugin “${pluginId}” already registered a nav entry at “${contribution.path}”`,
|
|
116
119
|
);
|
|
117
120
|
}
|
|
118
|
-
|
|
121
|
+
staged.navEntries.push({ ...contribution, pluginId });
|
|
119
122
|
},
|
|
120
123
|
};
|
|
124
|
+
}
|
|
121
125
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// Every nav entry must link to a page THIS plugin registers — staged in this
|
|
131
|
-
// call or already committed. A nav entry with no matching page is a guaranteed
|
|
132
|
-
// dead link (the plugin page surface renders "Page not found"), so reject it
|
|
133
|
-
// loudly. This throws BEFORE the commit loops, so it propagates out of
|
|
134
|
-
// `loadPlugin` and NOTHING (pages, panels, tabs, nav) commits — atomicity holds.
|
|
135
|
-
for (const navEntry of stagedNavEntries) {
|
|
126
|
+
/**
|
|
127
|
+
* Every nav entry must link to a page THIS plugin registers — staged in this call or
|
|
128
|
+
* already committed. A nav entry with no matching page is a guaranteed dead link (the
|
|
129
|
+
* plugin page surface renders "Page not found"), so reject it loudly. Throwing here —
|
|
130
|
+
* BEFORE any commit — keeps atomicity: nothing (pages, panels, tabs, nav) commits.
|
|
131
|
+
*/
|
|
132
|
+
function assertNavEntriesHavePages(pluginId: string, staged: StagedContributions): void {
|
|
133
|
+
for (const navEntry of staged.navEntries) {
|
|
136
134
|
const hasPage =
|
|
137
|
-
|
|
135
|
+
staged.pages.some((p) => p.path === navEntry.path) ||
|
|
138
136
|
pages.some((p) => p.pluginId === pluginId && p.path === navEntry.path);
|
|
139
137
|
if (!hasPage) {
|
|
140
138
|
throw new Error(
|
|
@@ -142,22 +140,45 @@ export async function loadPlugin(pluginId: string, entry: PluginEntry): Promise<
|
|
|
142
140
|
);
|
|
143
141
|
}
|
|
144
142
|
}
|
|
143
|
+
}
|
|
145
144
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
for (const [toolName, contribution] of
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Commit the staged contributions as one batch, so the registry only ever holds a
|
|
147
|
+
* plugin's complete set — never a partial set left behind by a mid-registration failure.
|
|
148
|
+
*/
|
|
149
|
+
function commitStaged(staged: StagedContributions): void {
|
|
150
|
+
for (const page of staged.pages) pages.push(page);
|
|
151
|
+
for (const [toolName, contribution] of staged.toolPanels) toolPanels.set(toolName, contribution);
|
|
152
|
+
for (const tab of staged.settingsTabs) settingsTabs.push(tab);
|
|
153
|
+
for (const navEntry of staged.navEntries) navEntries.push(navEntry);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Load one plugin: call its `register` entry with a context bound to `pluginId`,
|
|
158
|
+
* then commit everything it staged into the global registry.
|
|
159
|
+
*
|
|
160
|
+
* `entry` is AWAITED before commit, so a synchronous or an `async` entry is fully
|
|
161
|
+
* done first: a post-`await` throw skips the commit (atomicity holds for async too)
|
|
162
|
+
* and a post-`await` registration cannot slip past the batch. The seal is set after
|
|
163
|
+
* `entry` has SETTLED, so a later registration throws loudly; registrations made
|
|
164
|
+
* during `entry`'s synchronous body — including a microtask it enqueues then — are
|
|
165
|
+
* still captured and committed. A `register` that throws commits nothing.
|
|
166
|
+
*/
|
|
167
|
+
export async function loadPlugin(pluginId: string, entry: PluginEntry): Promise<void> {
|
|
168
|
+
const staged = emptyStaged();
|
|
169
|
+
const seal: Seal = { closed: false };
|
|
170
|
+
const context = makeStagingContext(pluginId, staged, seal);
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
await entry(context);
|
|
174
|
+
} finally {
|
|
175
|
+
// Whether `entry` resolved or threw, no further registration is valid — seal
|
|
176
|
+
// so a deferred call fails loudly rather than pushing into an orphaned array.
|
|
177
|
+
seal.closed = true;
|
|
160
178
|
}
|
|
179
|
+
|
|
180
|
+
assertNavEntriesHavePages(pluginId, staged);
|
|
181
|
+
commitStaged(staged);
|
|
161
182
|
}
|
|
162
183
|
|
|
163
184
|
/** The shell reads this after loading every installed plugin bundle. */
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `SecretRefField` — a data-agnostic masked editor for ONE secret value that is
|
|
3
3
|
* EITHER a reference to an existing env key OR a freshly pasted secret. It knows
|
|
4
|
-
* nothing about
|
|
5
|
-
*
|
|
6
|
-
* (the consumer that mounts this — the connectors page's McpServersSection —
|
|
7
|
-
* writes an `!ENV ${KEY}` marker for a `key` ref and runs the combined
|
|
8
|
-
* store-then-mark op for a `paste`).
|
|
4
|
+
* nothing about any wire format: it emits a discriminated {@link SecretRef} and
|
|
5
|
+
* the host maps that to whatever it stores.
|
|
9
6
|
*
|
|
10
7
|
* WRITE-ONLY for a pasted secret: the plaintext lives only in local editor state
|
|
11
8
|
* until the user commits it, at which point it leaves the DOM entirely (held only
|
|
@@ -29,10 +26,9 @@ import { Select } from '../components/select';
|
|
|
29
26
|
/**
|
|
30
27
|
* The value {@link SecretRefField} emits and reads back. The `source` discriminant
|
|
31
28
|
* is what the host branches on:
|
|
32
|
-
* - `key` — reference an existing env key; the host
|
|
29
|
+
* - `key` — reference an existing env key; the host resolves the reference.
|
|
33
30
|
* - `paste` — a new plaintext secret; the host stores it under a generated key
|
|
34
|
-
*
|
|
35
|
-
* write-only and is never rendered.
|
|
31
|
+
* and records the reference. `secret` is write-only and never rendered.
|
|
36
32
|
*/
|
|
37
33
|
export type SecretRef =
|
|
38
34
|
| { readonly source: 'key'; readonly key: string }
|
|
@@ -141,71 +137,125 @@ export function SecretRefField({
|
|
|
141
137
|
return (
|
|
142
138
|
<div data-testid={idPrefix}>
|
|
143
139
|
{canPick ? (
|
|
144
|
-
<
|
|
145
|
-
<Button
|
|
146
|
-
type="button"
|
|
147
|
-
variant={effectiveMode === 'key' ? 'primary' : 'secondary'}
|
|
148
|
-
aria-pressed={effectiveMode === 'key'}
|
|
149
|
-
onClick={() => {
|
|
150
|
-
setMode('key');
|
|
151
|
-
}}
|
|
152
|
-
>
|
|
153
|
-
Reference existing key
|
|
154
|
-
</Button>
|
|
155
|
-
<Button
|
|
156
|
-
type="button"
|
|
157
|
-
variant={effectiveMode === 'paste' ? 'primary' : 'secondary'}
|
|
158
|
-
aria-pressed={effectiveMode === 'paste'}
|
|
159
|
-
onClick={() => {
|
|
160
|
-
setMode('paste');
|
|
161
|
-
}}
|
|
162
|
-
>
|
|
163
|
-
Paste new secret
|
|
164
|
-
</Button>
|
|
165
|
-
</div>
|
|
140
|
+
<SecretSourceToggle label={label} effectiveMode={effectiveMode} onMode={setMode} />
|
|
166
141
|
) : null}
|
|
142
|
+
<SecretEditorBody
|
|
143
|
+
label={label}
|
|
144
|
+
effectiveMode={effectiveMode}
|
|
145
|
+
availableKeys={availableKeys}
|
|
146
|
+
pickedKey={pickedKey}
|
|
147
|
+
onPickKey={commitKey}
|
|
148
|
+
draftSecret={draftSecret}
|
|
149
|
+
onDraftChange={setDraftSecret}
|
|
150
|
+
onCommitPaste={commitPaste}
|
|
151
|
+
pasteDisabledReason={pasteDisabledReason}
|
|
152
|
+
pasteBlocked={pasteBlocked}
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
167
157
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
158
|
+
/** The key/paste source switch, shown only when key picking is available. */
|
|
159
|
+
function SecretSourceToggle({
|
|
160
|
+
label,
|
|
161
|
+
effectiveMode,
|
|
162
|
+
onMode,
|
|
163
|
+
}: {
|
|
164
|
+
label: string;
|
|
165
|
+
effectiveMode: 'key' | 'paste';
|
|
166
|
+
onMode: (mode: 'key' | 'paste') => void;
|
|
167
|
+
}): ReactNode {
|
|
168
|
+
return (
|
|
169
|
+
<div className="tai-row" role="group" aria-label={`${label} source`}>
|
|
170
|
+
<Button
|
|
171
|
+
type="button"
|
|
172
|
+
variant={effectiveMode === 'key' ? 'primary' : 'secondary'}
|
|
173
|
+
aria-pressed={effectiveMode === 'key'}
|
|
174
|
+
onClick={() => {
|
|
175
|
+
onMode('key');
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
Reference existing key
|
|
179
|
+
</Button>
|
|
180
|
+
<Button
|
|
181
|
+
type="button"
|
|
182
|
+
variant={effectiveMode === 'paste' ? 'primary' : 'secondary'}
|
|
183
|
+
aria-pressed={effectiveMode === 'paste'}
|
|
184
|
+
onClick={() => {
|
|
185
|
+
onMode('paste');
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
Paste new secret
|
|
189
|
+
</Button>
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The active editor: a key Select (or an empty-keys hint) or the write-only paste input. */
|
|
195
|
+
function SecretEditorBody({
|
|
196
|
+
label,
|
|
197
|
+
effectiveMode,
|
|
198
|
+
availableKeys,
|
|
199
|
+
pickedKey,
|
|
200
|
+
onPickKey,
|
|
201
|
+
draftSecret,
|
|
202
|
+
onDraftChange,
|
|
203
|
+
onCommitPaste,
|
|
204
|
+
pasteDisabledReason,
|
|
205
|
+
pasteBlocked,
|
|
206
|
+
}: {
|
|
207
|
+
label: string;
|
|
208
|
+
effectiveMode: 'key' | 'paste';
|
|
209
|
+
availableKeys: readonly string[];
|
|
210
|
+
pickedKey: string;
|
|
211
|
+
onPickKey: (key: string) => void;
|
|
212
|
+
draftSecret: string;
|
|
213
|
+
onDraftChange: (secret: string) => void;
|
|
214
|
+
onCommitPaste: () => void;
|
|
215
|
+
pasteDisabledReason: string | undefined;
|
|
216
|
+
pasteBlocked: boolean;
|
|
217
|
+
}): ReactNode {
|
|
218
|
+
if (effectiveMode === 'key') {
|
|
219
|
+
if (availableKeys.length === 0) {
|
|
220
|
+
return <span className="tai-field-hint">No keys available</span>;
|
|
221
|
+
}
|
|
222
|
+
return (
|
|
223
|
+
<Field label={label}>
|
|
224
|
+
<Select
|
|
225
|
+
value={pickedKey === '' ? undefined : pickedKey}
|
|
226
|
+
onValueChange={onPickKey}
|
|
227
|
+
placeholder="Select a key"
|
|
228
|
+
options={availableKeys.map((key) => ({ value: key, label: key }))}
|
|
229
|
+
/>
|
|
230
|
+
</Field>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return (
|
|
234
|
+
<div className="tai-row">
|
|
235
|
+
<div style={{ flex: 1 }}>
|
|
236
|
+
<Field label={label} error={pasteDisabledReason}>
|
|
237
|
+
{/* type=password: the plaintext is masked and offers no reveal —
|
|
238
|
+
it is write-only. */}
|
|
239
|
+
<TextInput
|
|
240
|
+
type="password"
|
|
241
|
+
value={draftSecret}
|
|
242
|
+
onChange={(event) => {
|
|
243
|
+
onDraftChange(event.target.value);
|
|
244
|
+
}}
|
|
245
|
+
autoComplete="off"
|
|
246
|
+
spellCheck={false}
|
|
247
|
+
placeholder="Paste a secret value"
|
|
248
|
+
/>
|
|
249
|
+
</Field>
|
|
250
|
+
</div>
|
|
251
|
+
<Button
|
|
252
|
+
type="button"
|
|
253
|
+
variant="primary"
|
|
254
|
+
disabled={draftSecret === '' || pasteBlocked}
|
|
255
|
+
onClick={onCommitPaste}
|
|
256
|
+
>
|
|
257
|
+
Use secret
|
|
258
|
+
</Button>
|
|
209
259
|
</div>
|
|
210
260
|
);
|
|
211
261
|
}
|