@tai42/studio-sdk 25.0.0 → 26.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -26
- package/dist/components/action-menu.d.ts +30 -0
- package/dist/components/action-menu.d.ts.map +1 -0
- package/dist/components/action-menu.js +101 -0
- package/dist/components/action-menu.js.map +1 -0
- package/dist/components/agent-picker.d.ts +22 -0
- package/dist/components/agent-picker.d.ts.map +1 -0
- package/dist/components/agent-picker.js +17 -0
- package/dist/components/agent-picker.js.map +1 -0
- package/dist/components/channel-picker.d.ts +27 -0
- package/dist/components/channel-picker.d.ts.map +1 -0
- package/dist/components/channel-picker.js +26 -0
- package/dist/components/channel-picker.js.map +1 -0
- package/dist/components/components.css +226 -8
- package/dist/components/confirm-dialog.d.ts +3 -1
- package/dist/components/confirm-dialog.d.ts.map +1 -1
- package/dist/components/confirm-dialog.js +2 -2
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/execution-key-picker.d.ts +26 -10
- package/dist/components/execution-key-picker.d.ts.map +1 -1
- package/dist/components/execution-key-picker.js +134 -15
- package/dist/components/execution-key-picker.js.map +1 -1
- package/dist/components/extension-combo-builder.d.ts +7 -7
- package/dist/components/extension-combo-builder.d.ts.map +1 -1
- package/dist/components/extension-combo-builder.js +42 -16
- package/dist/components/extension-combo-builder.js.map +1 -1
- package/dist/components/extension-combo-parts.d.ts +13 -11
- package/dist/components/extension-combo-parts.d.ts.map +1 -1
- package/dist/components/extension-combo-parts.js +10 -19
- package/dist/components/extension-combo-parts.js.map +1 -1
- package/dist/components/extension-config-editor.d.ts +33 -0
- package/dist/components/extension-config-editor.d.ts.map +1 -0
- package/dist/components/extension-config-editor.js +190 -0
- package/dist/components/extension-config-editor.js.map +1 -0
- package/dist/components/extension-grouping.d.ts +4 -41
- package/dist/components/extension-grouping.d.ts.map +1 -1
- package/dist/components/extension-grouping.js +1 -48
- package/dist/components/extension-grouping.js.map +1 -1
- package/dist/components/extension-picker.d.ts +4 -3
- package/dist/components/extension-picker.d.ts.map +1 -1
- package/dist/components/extension-picker.js +6 -6
- package/dist/components/extension-picker.js.map +1 -1
- package/dist/components/fleet-report.d.ts +8 -3
- package/dist/components/fleet-report.d.ts.map +1 -1
- package/dist/components/fleet-report.js +12 -2
- package/dist/components/fleet-report.js.map +1 -1
- package/dist/components/holder-kinds.d.ts +18 -0
- package/dist/components/holder-kinds.d.ts.map +1 -0
- package/dist/components/holder-kinds.js +41 -0
- package/dist/components/holder-kinds.js.map +1 -0
- package/dist/components/holder-list.d.ts +58 -0
- package/dist/components/holder-list.d.ts.map +1 -0
- package/dist/components/holder-list.js +77 -0
- package/dist/components/holder-list.js.map +1 -0
- package/dist/components/icons/index.d.ts +1 -1
- package/dist/components/icons/index.d.ts.map +1 -1
- package/dist/components/icons/index.js +1 -1
- package/dist/components/icons/index.js.map +1 -1
- package/dist/components/icons/nav-map.d.ts.map +1 -1
- package/dist/components/icons/nav-map.js +1 -3
- package/dist/components/icons/nav-map.js.map +1 -1
- package/dist/components/icons/nav-marks.d.ts +0 -4
- package/dist/components/icons/nav-marks.d.ts.map +1 -1
- package/dist/components/icons/nav-marks.js +0 -4
- package/dist/components/icons/nav-marks.js.map +1 -1
- package/dist/components/named-list-picker.d.ts +39 -0
- package/dist/components/named-list-picker.d.ts.map +1 -0
- package/dist/components/named-list-picker.js +61 -0
- package/dist/components/named-list-picker.js.map +1 -0
- package/dist/components/named-list-test-utils.d.ts +23 -0
- package/dist/components/named-list-test-utils.d.ts.map +1 -0
- package/dist/components/named-list-test-utils.js +33 -0
- package/dist/components/named-list-test-utils.js.map +1 -0
- package/dist/components/recycle-roll.d.ts +23 -0
- package/dist/components/recycle-roll.d.ts.map +1 -0
- package/dist/components/recycle-roll.js +47 -0
- package/dist/components/recycle-roll.js.map +1 -0
- package/dist/components/stepper.d.ts +28 -0
- package/dist/components/stepper.d.ts.map +1 -0
- package/dist/components/stepper.js +33 -0
- package/dist/components/stepper.js.map +1 -0
- package/dist/components/subject-section.d.ts +2 -1
- package/dist/components/subject-section.d.ts.map +1 -1
- package/dist/components/subject-section.js +10 -5
- package/dist/components/subject-section.js.map +1 -1
- package/dist/components/tokens.css +31 -41
- package/dist/components/tokens.d.ts +3 -4
- package/dist/components/tokens.d.ts.map +1 -1
- package/dist/components/tokens.js +5 -13
- package/dist/components/tokens.js.map +1 -1
- package/dist/components/tool-picker-parts.d.ts +5 -1
- package/dist/components/tool-picker-parts.d.ts.map +1 -1
- package/dist/components/tool-picker-parts.js +8 -6
- package/dist/components/tool-picker-parts.js.map +1 -1
- package/dist/components/tool-picker.d.ts +77 -2
- package/dist/components/tool-picker.d.ts.map +1 -1
- package/dist/components/tool-picker.js +87 -29
- package/dist/components/tool-picker.js.map +1 -1
- package/dist/components/version-history-panel.d.ts +7 -1
- package/dist/components/version-history-panel.d.ts.map +1 -1
- package/dist/components/version-history-panel.js +2 -2
- package/dist/components/version-history-panel.js.map +1 -1
- package/dist/components/version-history-rows.d.ts +4 -2
- package/dist/components/version-history-rows.d.ts.map +1 -1
- package/dist/components/version-history-rows.js +14 -2
- package/dist/components/version-history-rows.js.map +1 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +7 -0
- package/dist/errors.js.map +1 -1
- package/dist/hooks/useApi.d.ts +524 -197
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/hooks/useCapabilities.d.ts +43 -10
- package/dist/hooks/useCapabilities.d.ts.map +1 -1
- package/dist/hooks/useCapabilities.js +39 -10
- package/dist/hooks/useCapabilities.js.map +1 -1
- package/dist/index.d.ts +25 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -6
- package/dist/index.js.map +1 -1
- package/dist/marketplace-install/env-var-fields.d.ts +17 -0
- package/dist/marketplace-install/env-var-fields.d.ts.map +1 -0
- package/dist/marketplace-install/env-var-fields.js +22 -0
- package/dist/marketplace-install/env-var-fields.js.map +1 -0
- package/dist/marketplace-install/index.d.ts +18 -0
- package/dist/marketplace-install/index.d.ts.map +1 -0
- package/dist/marketplace-install/index.js +8 -0
- package/dist/marketplace-install/index.js.map +1 -0
- package/dist/marketplace-install/install-env.d.ts +21 -0
- package/dist/marketplace-install/install-env.d.ts.map +1 -0
- package/dist/marketplace-install/install-env.js +22 -0
- package/dist/marketplace-install/install-env.js.map +1 -0
- package/dist/marketplace-install/install-mount.d.ts +42 -0
- package/dist/marketplace-install/install-mount.d.ts.map +1 -0
- package/dist/marketplace-install/install-mount.js +62 -0
- package/dist/marketplace-install/install-mount.js.map +1 -0
- package/dist/marketplace-install/install-report.d.ts +14 -0
- package/dist/marketplace-install/install-report.d.ts.map +1 -0
- package/dist/marketplace-install/install-report.js +19 -0
- package/dist/marketplace-install/install-report.js.map +1 -0
- package/dist/marketplace-install/install-shell.d.ts +34 -0
- package/dist/marketplace-install/install-shell.d.ts.map +1 -0
- package/dist/marketplace-install/install-shell.js +32 -0
- package/dist/marketplace-install/install-shell.js.map +1 -0
- package/dist/marketplace-install/mount-install.d.ts +41 -0
- package/dist/marketplace-install/mount-install.d.ts.map +1 -0
- package/dist/marketplace-install/mount-install.js +90 -0
- package/dist/marketplace-install/mount-install.js.map +1 -0
- package/dist/marketplace-install/plugin-detail.d.ts +17 -0
- package/dist/marketplace-install/plugin-detail.d.ts.map +1 -0
- package/dist/marketplace-install/plugin-detail.js +41 -0
- package/dist/marketplace-install/plugin-detail.js.map +1 -0
- package/dist/marketplace-install/plugin-install.d.ts +27 -0
- package/dist/marketplace-install/plugin-install.d.ts.map +1 -0
- package/dist/marketplace-install/plugin-install.js +89 -0
- package/dist/marketplace-install/plugin-install.js.map +1 -0
- package/dist/marketplace-install/route-items.d.ts +14 -0
- package/dist/marketplace-install/route-items.d.ts.map +1 -0
- package/dist/marketplace-install/route-items.js +11 -0
- package/dist/marketplace-install/route-items.js.map +1 -0
- package/dist/marketplace-install/use-debounced-value.d.ts +6 -0
- package/dist/marketplace-install/use-debounced-value.d.ts.map +1 -0
- package/dist/marketplace-install/use-debounced-value.js +18 -0
- package/dist/marketplace-install/use-debounced-value.js.map +1 -0
- package/dist/marketplace-install/use-install-action.d.ts +26 -0
- package/dist/marketplace-install/use-install-action.d.ts.map +1 -0
- package/dist/marketplace-install/use-install-action.js +35 -0
- package/dist/marketplace-install/use-install-action.js.map +1 -0
- package/dist/marketplace-install/use-install-preview.d.ts +33 -0
- package/dist/marketplace-install/use-install-preview.d.ts.map +1 -0
- package/dist/marketplace-install/use-install-preview.js +44 -0
- package/dist/marketplace-install/use-install-preview.js.map +1 -0
- package/dist/marketplace-install/use-mount-install.d.ts +80 -0
- package/dist/marketplace-install/use-mount-install.d.ts.map +1 -0
- package/dist/marketplace-install/use-mount-install.js +75 -0
- package/dist/marketplace-install/use-mount-install.js.map +1 -0
- package/dist/navigation/index.d.ts +1 -1
- package/dist/navigation/index.d.ts.map +1 -1
- package/dist/navigation/index.js.map +1 -1
- package/dist/navigation/types.d.ts +23 -7
- package/dist/navigation/types.d.ts.map +1 -1
- package/dist/plugin/registry.d.ts +0 -8
- package/dist/plugin/registry.d.ts.map +1 -1
- package/dist/plugin/registry.js +25 -1
- package/dist/plugin/registry.js.map +1 -1
- package/dist/plugin/types.d.ts +36 -12
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/version.d.ts +6 -4
- package/dist/plugin/version.d.ts.map +1 -1
- package/dist/plugin/version.js +6 -4
- package/dist/plugin/version.js.map +1 -1
- package/dist/query-keys.d.ts +45 -4
- package/dist/query-keys.d.ts.map +1 -1
- package/dist/query-keys.js +52 -4
- package/dist/query-keys.js.map +1 -1
- package/dist/schema-form/json-schema-meta.d.ts +3 -0
- package/dist/schema-form/json-schema-meta.d.ts.map +1 -0
- package/dist/schema-form/json-schema-meta.js +3 -0
- package/dist/schema-form/json-schema-meta.js.map +1 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +2 -0
- package/dist/testing.js.map +1 -1
- package/dist/token-rules.d.ts +26 -0
- package/dist/token-rules.d.ts.map +1 -0
- package/dist/token-rules.js +64 -0
- package/dist/token-rules.js.map +1 -0
- package/package.json +7 -7
- package/src/components/action-menu.tsx +160 -0
- package/src/components/agent-picker.tsx +53 -0
- package/src/components/channel-picker.tsx +68 -0
- package/src/components/components.css +226 -8
- package/src/components/confirm-dialog.tsx +4 -1
- package/src/components/execution-key-picker.tsx +252 -36
- package/src/components/extension-combo-builder.tsx +61 -29
- package/src/components/extension-combo-parts.tsx +28 -38
- package/src/components/extension-config-editor.tsx +307 -0
- package/src/components/extension-grouping.ts +4 -73
- package/src/components/extension-picker.tsx +13 -13
- package/src/components/fleet-report.tsx +38 -4
- package/src/components/holder-kinds.ts +60 -0
- package/src/components/holder-list.tsx +163 -0
- package/src/components/icons/index.ts +0 -2
- package/src/components/icons/nav-map.ts +0 -4
- package/src/components/icons/nav-marks.tsx +0 -15
- package/src/components/named-list-picker.tsx +182 -0
- package/src/components/named-list-test-utils.tsx +47 -0
- package/src/components/recycle-roll.tsx +120 -0
- package/src/components/stepper.tsx +111 -0
- package/src/components/subject-section.tsx +10 -5
- package/src/components/tokens.css +31 -41
- package/src/components/tokens.ts +5 -13
- package/src/components/tool-picker-parts.tsx +17 -6
- package/src/components/tool-picker.tsx +215 -41
- package/src/components/version-history-panel.tsx +8 -0
- package/src/components/version-history-rows.tsx +14 -2
- package/src/errors.ts +10 -0
- package/src/hooks/useCapabilities.ts +59 -10
- package/src/index.ts +54 -14
- package/src/marketplace-install/env-var-fields.tsx +59 -0
- package/src/marketplace-install/index.ts +17 -0
- package/src/marketplace-install/install-env.ts +39 -0
- package/src/marketplace-install/install-mount.ts +96 -0
- package/src/marketplace-install/install-report.tsx +54 -0
- package/src/marketplace-install/install-shell.tsx +99 -0
- package/src/marketplace-install/mount-install.tsx +290 -0
- package/src/marketplace-install/plugin-detail.ts +60 -0
- package/src/marketplace-install/plugin-install.tsx +221 -0
- package/src/marketplace-install/route-items.ts +20 -0
- package/src/marketplace-install/use-debounced-value.ts +18 -0
- package/src/marketplace-install/use-install-action.ts +60 -0
- package/src/marketplace-install/use-install-preview.ts +74 -0
- package/src/marketplace-install/use-mount-install.ts +116 -0
- package/src/navigation/index.ts +1 -0
- package/src/navigation/types.ts +27 -7
- package/src/plugin/registry.ts +19 -1
- package/src/plugin/types.ts +40 -12
- package/src/plugin/version.ts +6 -4
- package/src/query-keys.ts +71 -4
- package/src/schema-form/json-schema-meta.ts +2 -0
- package/src/testing.ts +2 -0
- package/src/token-rules.ts +66 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The install preview (`POST /api/marketplace/install/preview`) as a plain fetch state
|
|
3
|
+
* machine through `useApi`: the SDK holds no query context, so each request gets its
|
|
4
|
+
* own `AbortController`, aborted when the request changes and on unmount. No retry,
|
|
5
|
+
* no cache.
|
|
6
|
+
*/
|
|
7
|
+
import type { MarketplaceInstallPreview } from '@tai42/api-client';
|
|
8
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
9
|
+
|
|
10
|
+
import { useApi } from '../hooks/useApi';
|
|
11
|
+
|
|
12
|
+
/** What a preview is asked for; `null` keeps the hook idle. */
|
|
13
|
+
export interface InstallPreviewRequest {
|
|
14
|
+
readonly ref: string;
|
|
15
|
+
readonly version: string | null;
|
|
16
|
+
readonly route_mounts?: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type InstallPreviewState =
|
|
20
|
+
| { readonly status: 'loading'; readonly previous: MarketplaceInstallPreview | null }
|
|
21
|
+
| { readonly status: 'ready'; readonly preview: MarketplaceInstallPreview }
|
|
22
|
+
| { readonly status: 'failed'; readonly error: unknown };
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Fire the preview whenever the serialized request changes. While a re-preview
|
|
26
|
+
* loads, `previous` keeps the last ready preview so the form does not blank on every
|
|
27
|
+
* edit; a failure clears it.
|
|
28
|
+
*/
|
|
29
|
+
export function useInstallPreview(request: InstallPreviewRequest | null): {
|
|
30
|
+
readonly state: InstallPreviewState;
|
|
31
|
+
readonly refetch: () => void;
|
|
32
|
+
} {
|
|
33
|
+
const api = useApi();
|
|
34
|
+
const [state, setState] = useState<InstallPreviewState>({ status: 'loading', previous: null });
|
|
35
|
+
const [attempt, setAttempt] = useState(0);
|
|
36
|
+
const requestKey = request === null ? null : JSON.stringify(request);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (requestKey === null) return;
|
|
40
|
+
const body = JSON.parse(requestKey) as InstallPreviewRequest;
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
setState((prev) => ({
|
|
43
|
+
status: 'loading',
|
|
44
|
+
previous:
|
|
45
|
+
prev.status === 'ready' ? prev.preview : prev.status === 'loading' ? prev.previous : null,
|
|
46
|
+
}));
|
|
47
|
+
api
|
|
48
|
+
.previewMarketplaceInstall(
|
|
49
|
+
{
|
|
50
|
+
ref: body.ref,
|
|
51
|
+
version: body.version ?? undefined,
|
|
52
|
+
...(body.route_mounts !== undefined ? { route_mounts: body.route_mounts } : {}),
|
|
53
|
+
},
|
|
54
|
+
controller.signal,
|
|
55
|
+
)
|
|
56
|
+
.then(
|
|
57
|
+
(preview) => {
|
|
58
|
+
if (!controller.signal.aborted) setState({ status: 'ready', preview });
|
|
59
|
+
},
|
|
60
|
+
(error: unknown) => {
|
|
61
|
+
if (!controller.signal.aborted) setState({ status: 'failed', error });
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
return () => {
|
|
65
|
+
controller.abort();
|
|
66
|
+
};
|
|
67
|
+
}, [api, requestKey, attempt]);
|
|
68
|
+
|
|
69
|
+
const refetch = useCallback(() => {
|
|
70
|
+
setAttempt((n) => n + 1);
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
return { state, refetch };
|
|
74
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The view-model for the route-mounting install / update form: the per-item base
|
|
3
|
+
* state, the debounced live preview, the derived collision/public/env surfaces, and
|
|
4
|
+
* the body the submit sends.
|
|
5
|
+
*/
|
|
6
|
+
import type { MarketplaceInstallPreview } from '@tai42/api-client';
|
|
7
|
+
import { useMemo, useState } from 'react';
|
|
8
|
+
|
|
9
|
+
import { buildInstallExtras, deriveEnvSecretMap, type InstallExtras } from './install-mount';
|
|
10
|
+
import type { RouteItem } from './route-items';
|
|
11
|
+
import { useDebouncedValue } from './use-debounced-value';
|
|
12
|
+
import { type InstallPreviewState, useInstallPreview } from './use-install-preview';
|
|
13
|
+
|
|
14
|
+
/** How long after the last base keystroke the preview refetches. */
|
|
15
|
+
const PREVIEW_DEBOUNCE_MS = 250;
|
|
16
|
+
|
|
17
|
+
/** The inputs the mount form hands its view-model. */
|
|
18
|
+
export interface UseMountInstallProps {
|
|
19
|
+
readonly refValue: string;
|
|
20
|
+
readonly version: string | null;
|
|
21
|
+
readonly verb: 'Install' | 'Update';
|
|
22
|
+
readonly routeItems: readonly RouteItem[];
|
|
23
|
+
readonly storedMounts?: Record<string, string>;
|
|
24
|
+
readonly requiredEnvSecret?: Record<string, boolean>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The preview on screen: the ready one, else the last ready one while a remap re-previews. */
|
|
28
|
+
function shownPreview(state: InstallPreviewState): MarketplaceInstallPreview | undefined {
|
|
29
|
+
if (state.status === 'ready') return state.preview;
|
|
30
|
+
if (state.status === 'loading') return state.previous ?? undefined;
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useMountInstall(props: UseMountInstallProps) {
|
|
35
|
+
const { refValue, version, verb, routeItems, storedMounts, requiredEnvSecret } = props;
|
|
36
|
+
|
|
37
|
+
// The base each item is CURRENTLY mounted at: the stored mount on update, the
|
|
38
|
+
// declared default on install (or for an item the install added). This seeds the
|
|
39
|
+
// inputs and, on update, is the baseline the submit diffs against so an untouched
|
|
40
|
+
// base is OMITTED and the server preserves it.
|
|
41
|
+
const seededBases = useMemo(
|
|
42
|
+
() =>
|
|
43
|
+
Object.fromEntries(
|
|
44
|
+
routeItems.map((item) => [item.name, storedMounts?.[item.name] ?? item.routes.base]),
|
|
45
|
+
),
|
|
46
|
+
[routeItems, storedMounts],
|
|
47
|
+
);
|
|
48
|
+
const [bases, setBases] = useState<Record<string, string>>(seededBases);
|
|
49
|
+
const [accepted, setAccepted] = useState(false);
|
|
50
|
+
const [envValues, setEnvValues] = useState<Record<string, string>>({});
|
|
51
|
+
// Only OPERATOR overrides live in state; each toggle's baseline is the derived
|
|
52
|
+
// `requiredEnvSecret`, so a var starts secret iff the server marks it so — no
|
|
53
|
+
// async seed to miss when the preview lands.
|
|
54
|
+
const [envSecretOverride, setEnvSecretOverride] = useState<Record<string, boolean>>({});
|
|
55
|
+
|
|
56
|
+
// The serialized map is the debounce key: a stable string that only changes when a
|
|
57
|
+
// base actually changes, so identity churn never re-previews.
|
|
58
|
+
const debouncedKey = useDebouncedValue(JSON.stringify(bases), PREVIEW_DEBOUNCE_MS);
|
|
59
|
+
const request = useMemo(
|
|
60
|
+
() => ({
|
|
61
|
+
ref: refValue,
|
|
62
|
+
version,
|
|
63
|
+
route_mounts: JSON.parse(debouncedKey) as Record<string, string>,
|
|
64
|
+
}),
|
|
65
|
+
[refValue, version, debouncedKey],
|
|
66
|
+
);
|
|
67
|
+
const { state } = useInstallPreview(request);
|
|
68
|
+
|
|
69
|
+
const preview = shownPreview(state);
|
|
70
|
+
const previewIsError = state.status === 'failed';
|
|
71
|
+
const collisions = preview?.collisions ?? [];
|
|
72
|
+
const publicRows = preview?.new_public_routes ?? [];
|
|
73
|
+
const requiresAccept = preview?.requires_public_acceptance ?? false;
|
|
74
|
+
// The env to collect is the preview's server-computed missing set — only on
|
|
75
|
+
// INSTALL: an update that would add a required var is refused server-side, so the
|
|
76
|
+
// update flow never collects env.
|
|
77
|
+
const envToCollect = verb === 'Install' ? (preview?.missing_env ?? []) : [];
|
|
78
|
+
const envSecretMap = deriveEnvSecretMap(
|
|
79
|
+
envToCollect,
|
|
80
|
+
preview,
|
|
81
|
+
requiredEnvSecret,
|
|
82
|
+
envSecretOverride,
|
|
83
|
+
);
|
|
84
|
+
// Submit is blocked until a clean preview exists with no collision and any
|
|
85
|
+
// required public acceptance given. A preview error blocks too — an unverified
|
|
86
|
+
// mount must never be committed.
|
|
87
|
+
const blocked =
|
|
88
|
+
previewIsError ||
|
|
89
|
+
preview === undefined ||
|
|
90
|
+
collisions.length > 0 ||
|
|
91
|
+
(requiresAccept && !accepted);
|
|
92
|
+
|
|
93
|
+
const extras = (): InstallExtras =>
|
|
94
|
+
buildInstallExtras(verb, bases, seededBases, accepted, envToCollect, envValues, envSecretMap);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
bases,
|
|
98
|
+
setBases,
|
|
99
|
+
accepted,
|
|
100
|
+
setAccepted,
|
|
101
|
+
envValues,
|
|
102
|
+
setEnvValues,
|
|
103
|
+
setEnvSecretOverride,
|
|
104
|
+
preview,
|
|
105
|
+
previewIsError,
|
|
106
|
+
collisions,
|
|
107
|
+
publicRows,
|
|
108
|
+
requiresAccept,
|
|
109
|
+
envToCollect,
|
|
110
|
+
envSecretMap,
|
|
111
|
+
blocked,
|
|
112
|
+
extras,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export type MountInstallModel = ReturnType<typeof useMountInstall>;
|
package/src/navigation/index.ts
CHANGED
package/src/navigation/types.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* supplies the runtime resolution via {@link NavigationContextValue}; features
|
|
13
13
|
* consume it type-safely.
|
|
14
14
|
*/
|
|
15
|
-
import type { ConversationDeliveryStatus } from '@tai42/api-client';
|
|
15
|
+
import type { ConversationDeliveryStatus, ReferenceHolder } from '@tai42/api-client';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Every top-level route the shell owns, mapped to its typed search parameters.
|
|
@@ -24,7 +24,8 @@ export interface RouteSearchByToken {
|
|
|
24
24
|
login: Record<string, never>;
|
|
25
25
|
tools: { tool?: string; tags?: string[]; q?: string };
|
|
26
26
|
agents: Record<string, never>;
|
|
27
|
-
|
|
27
|
+
// `preset` opens one preset; `version` marks one row of its version history.
|
|
28
|
+
presets: { preset?: string; version?: number };
|
|
28
29
|
// The states surface: `state` opens one state, `tab` picks its section, and
|
|
29
30
|
// `subject` (`<kind>:<key>`, split on the FIRST `:`, key URL-encoded) with `target`
|
|
30
31
|
// (`<target_kind>:<target_name>`, same split rule) together address one subject's
|
|
@@ -39,7 +40,6 @@ export interface RouteSearchByToken {
|
|
|
39
40
|
// of the state list — a read-only view of the whole uploaded document.
|
|
40
41
|
template?: string;
|
|
41
42
|
};
|
|
42
|
-
extensions: Record<string, never>;
|
|
43
43
|
interactions: Record<string, never>;
|
|
44
44
|
notifications: Record<string, never>;
|
|
45
45
|
// The conversation monitor's drill state + thread-list filters: `route` picks a
|
|
@@ -53,21 +53,26 @@ export interface RouteSearchByToken {
|
|
|
53
53
|
status?: ConversationDeliveryStatus;
|
|
54
54
|
address?: string;
|
|
55
55
|
q?: string;
|
|
56
|
+
// `config` (`<target_kind>:<target_name>`, split on the FIRST `:`) opens the
|
|
57
|
+
// per-target configs tab and marks that config's row.
|
|
58
|
+
config?: string;
|
|
56
59
|
};
|
|
57
60
|
connectors: { connection?: string };
|
|
58
61
|
// The Served Endpoints surface (the promoted sub-MCP servers): a curated subset
|
|
59
62
|
// of tools served on a transport. No search parameters — a flat list + create form.
|
|
60
63
|
servedEndpoints: Record<string, never>;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
// `hook` marks one hook's row.
|
|
65
|
+
hooks: { hook?: string };
|
|
66
|
+
// `id` opens one stored object's detail.
|
|
67
|
+
storage: { q?: string; id?: string };
|
|
64
68
|
manifest: Record<string, never>;
|
|
65
69
|
// The generic Members listing: people + outstanding invitations aggregated across
|
|
66
70
|
// every accounts provider. No search parameters — a flat read-only view.
|
|
67
71
|
members: Record<string, never>;
|
|
68
72
|
settings: Record<string, never>;
|
|
69
73
|
system: Record<string, never>;
|
|
70
|
-
|
|
74
|
+
// `schedule` marks one schedule's row.
|
|
75
|
+
scheduling: { schedule?: string };
|
|
71
76
|
observability: {
|
|
72
77
|
tab?: 'dashboard' | 'tracing';
|
|
73
78
|
from?: string;
|
|
@@ -225,6 +230,21 @@ export interface NavigationContextValue {
|
|
|
225
230
|
* {@link navigatePluginWithOptions}; reached through {@link usePluginEntryNavigation}.
|
|
226
231
|
*/
|
|
227
232
|
updatePluginEntryState?: (pluginId: string, state: unknown) => void;
|
|
233
|
+
/**
|
|
234
|
+
* The plugin page that shows one holder of a plugin-declared kind, from the resolver
|
|
235
|
+
* a plugin registered for that kind; `null` when no plugin resolves it. OPTIONAL for
|
|
236
|
+
* the same additive reason as {@link navigatePluginWithOptions}: a host that omits
|
|
237
|
+
* it links no plugin kind, and the holder renders as text.
|
|
238
|
+
*/
|
|
239
|
+
resolveHolderLink?: (holder: ReferenceHolder) => PluginHolderTarget | null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** A plugin page addressed by {@link NavigationContextValue.resolveHolderLink}. */
|
|
243
|
+
export interface PluginHolderTarget {
|
|
244
|
+
readonly pluginId: string;
|
|
245
|
+
readonly pagePath: string;
|
|
246
|
+
readonly params?: string;
|
|
247
|
+
readonly search?: PluginSearch;
|
|
228
248
|
}
|
|
229
249
|
|
|
230
250
|
/**
|
package/src/plugin/registry.ts
CHANGED
|
@@ -6,12 +6,15 @@
|
|
|
6
6
|
* the SAME instance every host caller — the shell and the feature panels — sees. The
|
|
7
7
|
* plugin surface (`@tai42/studio-sdk`) does not include it, so a plugin cannot reach it.
|
|
8
8
|
*/
|
|
9
|
+
import { CORE_HOLDER_KINDS } from '../components/holder-kinds';
|
|
9
10
|
import type {
|
|
11
|
+
HolderLinkContribution,
|
|
10
12
|
NavEntryContribution,
|
|
11
13
|
PageContribution,
|
|
12
14
|
PluginContext,
|
|
13
15
|
PluginContributions,
|
|
14
16
|
PluginEntry,
|
|
17
|
+
RegisteredHolderLink,
|
|
15
18
|
RegisteredNavEntry,
|
|
16
19
|
RegisteredPage,
|
|
17
20
|
RegisteredRoutePanel,
|
|
@@ -26,6 +29,7 @@ const pages: RegisteredPage[] = [];
|
|
|
26
29
|
const settingsTabs: RegisteredSettingsTab[] = [];
|
|
27
30
|
const navEntries: RegisteredNavEntry[] = [];
|
|
28
31
|
const routePanels = new Map<string, RegisteredRoutePanel>();
|
|
32
|
+
const holderLinks = new Map<string, RegisteredHolderLink>();
|
|
29
33
|
|
|
30
34
|
/**
|
|
31
35
|
* What the host knows about a plugin from its registry listing entry: `mounts`, the
|
|
@@ -46,6 +50,7 @@ interface StagedContributions {
|
|
|
46
50
|
readonly tabIds: Set<string>;
|
|
47
51
|
readonly navEntries: RegisteredNavEntry[];
|
|
48
52
|
readonly routePanels: Map<string, RegisteredRoutePanel>;
|
|
53
|
+
readonly holderLinks: Map<string, RegisteredHolderLink>;
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
/**
|
|
@@ -64,6 +69,7 @@ function emptyStaged(): StagedContributions {
|
|
|
64
69
|
tabIds: new Set(),
|
|
65
70
|
navEntries: [],
|
|
66
71
|
routePanels: new Map(),
|
|
72
|
+
holderLinks: new Map(),
|
|
67
73
|
};
|
|
68
74
|
}
|
|
69
75
|
|
|
@@ -144,6 +150,16 @@ function makeStagingContext(
|
|
|
144
150
|
}
|
|
145
151
|
staged.routePanels.set(contribution.channel, { ...contribution, pluginId });
|
|
146
152
|
},
|
|
153
|
+
registerHolderLink(contribution: HolderLinkContribution): void {
|
|
154
|
+
assertOpen();
|
|
155
|
+
if (CORE_HOLDER_KINDS.has(contribution.kind)) {
|
|
156
|
+
throw new Error(`holder kind “${contribution.kind}” is a core kind; the shell links it`);
|
|
157
|
+
}
|
|
158
|
+
if (staged.holderLinks.has(contribution.kind) || holderLinks.has(contribution.kind)) {
|
|
159
|
+
throw new Error(`a holder link is already registered for kind “${contribution.kind}”`);
|
|
160
|
+
}
|
|
161
|
+
staged.holderLinks.set(contribution.kind, { ...contribution, pluginId });
|
|
162
|
+
},
|
|
147
163
|
routeBase(itemName: string): string | null {
|
|
148
164
|
return Object.hasOwn(host.mounts, itemName) ? (host.mounts[itemName] ?? null) : null;
|
|
149
165
|
},
|
|
@@ -200,6 +216,7 @@ function commitStaged(staged: StagedContributions): void {
|
|
|
200
216
|
for (const tab of staged.settingsTabs) settingsTabs.push(tab);
|
|
201
217
|
for (const navEntry of staged.navEntries) navEntries.push(navEntry);
|
|
202
218
|
for (const [channel, panel] of staged.routePanels) routePanels.set(channel, panel);
|
|
219
|
+
for (const [kind, link] of staged.holderLinks) holderLinks.set(kind, link);
|
|
203
220
|
}
|
|
204
221
|
|
|
205
222
|
/**
|
|
@@ -237,7 +254,7 @@ export async function loadPlugin(
|
|
|
237
254
|
|
|
238
255
|
/** The shell reads this after loading every installed plugin bundle. */
|
|
239
256
|
export function getContributions(): PluginContributions {
|
|
240
|
-
return { toolPanels, pages, settingsTabs, navEntries, routePanels };
|
|
257
|
+
return { toolPanels, pages, settingsTabs, navEntries, routePanels, holderLinks };
|
|
241
258
|
}
|
|
242
259
|
|
|
243
260
|
/** Tests reset the module-level registry between cases. */
|
|
@@ -247,4 +264,5 @@ export function __resetContributions(): void {
|
|
|
247
264
|
settingsTabs.length = 0;
|
|
248
265
|
navEntries.length = 0;
|
|
249
266
|
routePanels.clear();
|
|
267
|
+
holderLinks.clear();
|
|
250
268
|
}
|
package/src/plugin/types.ts
CHANGED
|
@@ -20,9 +20,11 @@
|
|
|
20
20
|
* its own. Bundled third-party base CSS is exempt from rule (c) but rules
|
|
21
21
|
* (a) and (b) still bind it.
|
|
22
22
|
*/
|
|
23
|
-
import type { ConversationRouteView } from '@tai42/api-client';
|
|
23
|
+
import type { ConversationRouteView, ReferenceHolder } from '@tai42/api-client';
|
|
24
24
|
import type { ComponentType } from 'react';
|
|
25
25
|
|
|
26
|
+
import type { PluginSearch } from '../navigation/types';
|
|
27
|
+
|
|
26
28
|
/**
|
|
27
29
|
* A capability requirement a contribution may declare. `routes` is a list of
|
|
28
30
|
* route-path prefixes (anyOf semantics, the same evaluator the shell nav uses):
|
|
@@ -142,16 +144,14 @@ export interface RegisteredPage extends PageContribution {
|
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
/**
|
|
145
|
-
* The sidebar section a nav entry renders in.
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* placed after the core sections.
|
|
150
|
-
* (a bundle newer than this host), falling back to that shared section, so the
|
|
151
|
-
* field is a placement hint, never a hard contract.
|
|
147
|
+
* The sidebar section a nav entry renders in. The core sections render in this
|
|
148
|
+
* order: Capabilities, Connections, Doors, Activity, Administration. A named core
|
|
149
|
+
* section groups the entry there, after that section's core rows. An absent field
|
|
150
|
+
* or any value that is not a core section renders the entry in the shared
|
|
151
|
+
* "Plugins" section, placed after the core sections.
|
|
152
152
|
*/
|
|
153
153
|
export type NavEntrySection =
|
|
154
|
-
'Capabilities' | 'Connections' | '
|
|
154
|
+
'Capabilities' | 'Connections' | 'Doors' | 'Activity' | 'Administration';
|
|
155
155
|
|
|
156
156
|
export interface NavEntryContribution {
|
|
157
157
|
/**
|
|
@@ -162,9 +162,8 @@ export interface NavEntryContribution {
|
|
|
162
162
|
readonly path: string;
|
|
163
163
|
readonly title: string;
|
|
164
164
|
/**
|
|
165
|
-
* Optional target sidebar section (see {@link NavEntrySection});
|
|
166
|
-
* shared "Plugins" section.
|
|
167
|
-
* on the same plugin API version.
|
|
165
|
+
* Optional target sidebar section (see {@link NavEntrySection}); omitted or
|
|
166
|
+
* unrecognised ⇒ the shared "Plugins" section.
|
|
168
167
|
*/
|
|
169
168
|
readonly section?: NavEntrySection;
|
|
170
169
|
/**
|
|
@@ -249,6 +248,28 @@ export interface RegisteredRoutePanel extends RoutePanelContribution {
|
|
|
249
248
|
readonly pluginId: string;
|
|
250
249
|
}
|
|
251
250
|
|
|
251
|
+
/** Where a plugin's page shows one holder: a page of the registering plugin. */
|
|
252
|
+
export interface HolderLinkTarget {
|
|
253
|
+
readonly pagePath: string;
|
|
254
|
+
readonly params?: string;
|
|
255
|
+
readonly search?: PluginSearch;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Resolves the holders of one plugin-declared kind to a page of the registering
|
|
260
|
+
* plugin, so a holder list links that holder. One resolver per kind deployment-wide;
|
|
261
|
+
* a core kind cannot be registered.
|
|
262
|
+
*/
|
|
263
|
+
export interface HolderLinkContribution {
|
|
264
|
+
readonly kind: string;
|
|
265
|
+
readonly resolve: (holder: ReferenceHolder) => HolderLinkTarget;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** A holder-link resolver as stored in the registry, stamped with its plugin. */
|
|
269
|
+
export interface RegisteredHolderLink extends HolderLinkContribution {
|
|
270
|
+
readonly pluginId: string;
|
|
271
|
+
}
|
|
272
|
+
|
|
252
273
|
/** Everything a single plugin bundle has registered at import time. */
|
|
253
274
|
export interface PluginContributions {
|
|
254
275
|
readonly toolPanels: ReadonlyMap<string, ToolPanelContribution>;
|
|
@@ -257,6 +278,8 @@ export interface PluginContributions {
|
|
|
257
278
|
readonly navEntries: readonly RegisteredNavEntry[];
|
|
258
279
|
/** Route panels keyed by the channel they decorate. */
|
|
259
280
|
readonly routePanels: ReadonlyMap<string, RegisteredRoutePanel>;
|
|
281
|
+
/** Holder-link resolvers keyed by the holder kind they resolve. */
|
|
282
|
+
readonly holderLinks: ReadonlyMap<string, RegisteredHolderLink>;
|
|
260
283
|
}
|
|
261
284
|
|
|
262
285
|
/**
|
|
@@ -287,6 +310,11 @@ export interface PluginContext {
|
|
|
287
310
|
* same channels under `contributions.route_panels`, or the plugin fails to load.
|
|
288
311
|
*/
|
|
289
312
|
registerRoutePanel(contribution: RoutePanelContribution): void;
|
|
313
|
+
/**
|
|
314
|
+
* Register the page that shows the holders of one plugin-declared kind. Registering a
|
|
315
|
+
* kind twice (by any plugin) or a core kind throws.
|
|
316
|
+
*/
|
|
317
|
+
registerHolderLink(contribution: HolderLinkContribution): void;
|
|
290
318
|
/**
|
|
291
319
|
* The resolved absolute base of one of this plugin's route items, e.g. `/api/channels/web`,
|
|
292
320
|
* or `null` when the item has no mount on this deployment (an optional router not listed, a
|
package/src/plugin/version.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Studio-plugin API compatibility version.
|
|
3
3
|
*
|
|
4
|
-
* A monotonic integer bumped ONLY on a breaking change to the Studio-plugin API
|
|
5
|
-
*
|
|
6
|
-
* rename/removal bumps it; genuinely internal/visual changes do not)
|
|
4
|
+
* A monotonic integer bumped ONLY on a breaking change to the Studio-plugin API,
|
|
5
|
+
* to the public prop/type surface of any DS component the SDK exports (a prop
|
|
6
|
+
* rename/removal bumps it; genuinely internal/visual changes do not), or to the
|
|
7
|
+
* design-token names (`TOKEN_NAMES`), the only names a plugin styles through (a
|
|
8
|
+
* rename or a removal bumps it; a new name does not).
|
|
7
9
|
*
|
|
8
10
|
* Gate rule: equality is correct precisely because additive changes never
|
|
9
11
|
* bump (existing plugins keep matching) and after a break every plugin must
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
* authoring docs state it: stylesheet injection is a host capability, so a
|
|
25
27
|
* deployment pairs the Studio host with plugins built for it.
|
|
26
28
|
*/
|
|
27
|
-
export const STUDIO_PLUGIN_API_VERSION =
|
|
29
|
+
export const STUDIO_PLUGIN_API_VERSION = 2;
|
|
28
30
|
|
|
29
31
|
export interface VersionGateResult {
|
|
30
32
|
readonly ok: boolean;
|
package/src/query-keys.ts
CHANGED
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
export const toolsListKey = ['tools'] as const;
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* The extension catalog (`GET /api/extensions`). The
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* The extension catalog (`GET /api/extensions`). The manifest feature keys its
|
|
18
|
+
* loaded-extensions list on it; a tool's extension-combo save (in the tools feature)
|
|
19
|
+
* invalidates it too — a cross-feature reference that must resolve to one
|
|
20
|
+
* authoritative constant.
|
|
21
21
|
*/
|
|
22
22
|
export const extensionsQueryKey = ['extensions'] as const;
|
|
23
23
|
|
|
@@ -33,6 +33,13 @@ export const subMcpKey = ['sub-mcp'] as const;
|
|
|
33
33
|
/** The api-key payloads; read by settings AND the hooks execution-key picker. */
|
|
34
34
|
export const tokensPayloadKey = ['auth-tokens-payload'] as const;
|
|
35
35
|
|
|
36
|
+
/** The installed channel plugins (`GET /api/channels`): the interactions feature's Delivery channels card and the
|
|
37
|
+
* conversations feature's route form read it under this one key. */
|
|
38
|
+
export const channelsListKey = ['channels'] as const;
|
|
39
|
+
|
|
40
|
+
/** The registered-agent list (`GET /api/agents`): the agents feature and the conversations feature's route form. */
|
|
41
|
+
export const agentsListKey = ['agents', 'list'] as const;
|
|
42
|
+
|
|
36
43
|
/**
|
|
37
44
|
* The state catalog (`GET /api/states`). The states feature owns it, and every door
|
|
38
45
|
* screen that hosts the state-binding editor (a preset, a route, a hook, a schedule)
|
|
@@ -47,3 +54,63 @@ export const statesListKey = ['states', 'list'] as const;
|
|
|
47
54
|
* keyed here so the two never fetch the same catalog under diverging tuples.
|
|
48
55
|
*/
|
|
49
56
|
export const stateTemplatesKey = ['states', 'state-templates'] as const;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The holders that would refuse detaching `template` from `state`
|
|
60
|
+
* (`GET /api/states/{name}/attachments/{template}/referees`), read by the detach confirm.
|
|
61
|
+
*/
|
|
62
|
+
export function stateTemplateRefereesKey(
|
|
63
|
+
state: string,
|
|
64
|
+
template: string,
|
|
65
|
+
): readonly ['states', 'referees', string, string] {
|
|
66
|
+
return ['states', 'referees', state, template];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// -- MCP servers (read by the Manifest screen's MCP block and the Connectors directory)
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The PRESERVED manifest (`GET /api/manifest/preserved`): `!ENV ${KEY}` markers intact,
|
|
73
|
+
* no resolved secrets. Its own key, never the resolved manifest's, so the two views
|
|
74
|
+
* never collide in the cache.
|
|
75
|
+
*/
|
|
76
|
+
export const preservedManifestKey = ['manifest', 'preserved'] as const;
|
|
77
|
+
|
|
78
|
+
/** The mounted MCP servers' live status (`GET /api/mcp-status`). */
|
|
79
|
+
export const mcpStatusKey = ['mcp-status'] as const;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The fleet-wide failed-MCP roster (`GET /api/mcp-status/failed`). A reload or
|
|
83
|
+
* deregister invalidates it together with {@link mcpStatusKey}, since a re-attached
|
|
84
|
+
* server moves between the two views.
|
|
85
|
+
*/
|
|
86
|
+
export const mcpFailedKey = ['mcp-status', 'failed'] as const;
|
|
87
|
+
|
|
88
|
+
/** One server's advertised tools (`POST /api/mcp-status/{title}/advertised-tools`). */
|
|
89
|
+
export function mcpAdvertisedToolsKey(
|
|
90
|
+
title: string,
|
|
91
|
+
): readonly ['mcp-status', 'advertised-tools', string] {
|
|
92
|
+
return ['mcp-status', 'advertised-tools', title];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The MCP entry config schema (`GET /api/mcp-config/schema`). */
|
|
96
|
+
export const mcpConfigSchemaKey = ['mcp-config-schema'] as const;
|
|
97
|
+
|
|
98
|
+
/** The extension catalog the MCP tool composer draws from (`GET /api/extensions`). */
|
|
99
|
+
export const mcpExtensionsKey = ['mcp-extensions'] as const;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The manifest MCP section's `!ENV` marker refs (`GET /api/manifest/mcp-env-refs`):
|
|
103
|
+
* names and set/unset booleans only, never values.
|
|
104
|
+
*/
|
|
105
|
+
export const mcpEnvRefsKey = ['mcp-env-refs'] as const;
|
|
106
|
+
|
|
107
|
+
/** The installed marketplace inventory (`GET /api/marketplace/installed`). */
|
|
108
|
+
export const installedMarketplacePluginsKey = ['marketplace', 'installed'] as const;
|
|
109
|
+
|
|
110
|
+
/** One marketplace plugin's detail (`GET /api/marketplace/plugins/{ns}/{name}`), by its ref. */
|
|
111
|
+
export function marketplacePluginKey(ref: string): readonly ['marketplace', 'plugin', string] {
|
|
112
|
+
return ['marketplace', 'plugin', ref];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The deployment env map (`GET /api/config/env` → `{ env, secret_keys }`). */
|
|
116
|
+
export const envConfigKey = ['env-config'] as const;
|
package/src/testing.ts
CHANGED
|
@@ -4,6 +4,8 @@ export { __resetContributions } from './plugin/registry';
|
|
|
4
4
|
export { __resetPluginHostState } from './plugin/host-state';
|
|
5
5
|
/** Test-only injector: serves a fixed tool display-name map without a live fetch. */
|
|
6
6
|
export { StaticToolDisplayNamesProvider } from './hooks/useToolDisplayNames';
|
|
7
|
+
/** The destructive-not-text rule over one source text, for a host's own source gate. */
|
|
8
|
+
export { destructiveColorOffences } from './token-rules';
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Every `ResizeObserver` the stub has handed out and not yet had disconnected,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source rules over the design-token names, shared by the repo's token-usage gate and
|
|
3
|
+
* by any host that scans its own sources with the same rule (exported from
|
|
4
|
+
* `@tai42/studio-sdk/testing`). Each rule reads one source text and returns its
|
|
5
|
+
* offences; which files to read is the caller's choice, the rule never varies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The property a declaration's value belongs to: the last `name:` before the
|
|
10
|
+
* value, with no statement boundary in between. Written as a backwards search
|
|
11
|
+
* rather than a forward `property: value` match so that a value containing its
|
|
12
|
+
* own commas and parentheses — `linear-gradient(90deg, …)`, or the next entry of
|
|
13
|
+
* a JSX style object — cannot make the scan attribute the value to the wrong
|
|
14
|
+
* property or miss it entirely.
|
|
15
|
+
*/
|
|
16
|
+
export function governingProperty(source: string, valueIndex: number): string | undefined {
|
|
17
|
+
const match = /([-A-Za-z][\w-]*)\s*:\s*[^;{}:]*$/.exec(source.slice(0, valueIndex));
|
|
18
|
+
return match?.[1];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Every reference to a destructive role token, in a stylesheet or a JSX inline style. */
|
|
22
|
+
const DESTRUCTIVE_REFERENCE = /var\(\s*--tai-color-(?:on-)?destructive/g;
|
|
23
|
+
|
|
24
|
+
/** The one rule whose label sits on the destructive fill. */
|
|
25
|
+
const DESTRUCTIVE_BUTTON_SELECTOR = /^\.tai-btn-danger(?![\w-])/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether the declaration at `valueIndex` sits inside a CSS rule whose every
|
|
29
|
+
* selector is the destructive button. The selector is the text between the
|
|
30
|
+
* previous statement boundary and the innermost open brace, with comments removed.
|
|
31
|
+
*/
|
|
32
|
+
function insideDestructiveButtonRule(source: string, valueIndex: number): boolean {
|
|
33
|
+
const before = source.slice(0, valueIndex);
|
|
34
|
+
const open = before.lastIndexOf('{');
|
|
35
|
+
if (open === -1 || before.lastIndexOf('}') > open) return false;
|
|
36
|
+
const head = before.slice(0, open);
|
|
37
|
+
const start = Math.max(head.lastIndexOf('}'), head.lastIndexOf('{'), head.lastIndexOf(';')) + 1;
|
|
38
|
+
const selectors = head
|
|
39
|
+
.slice(start)
|
|
40
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
41
|
+
.split(',')
|
|
42
|
+
.map((selector) => selector.trim());
|
|
43
|
+
return selectors.every((selector) => DESTRUCTIVE_BUTTON_SELECTOR.test(selector));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The properties in `source` that paint text with a destructive role token, in the
|
|
48
|
+
* order they appear. The destructive tokens are the fill, its hover and the label on
|
|
49
|
+
* that fill: a fill colour as text fails contrast on the dark grounds, so error text
|
|
50
|
+
* takes `--tai-color-err-text` instead. A `color` declaration (CSS or JSX) is an
|
|
51
|
+
* offence anywhere but the `.tai-btn-danger` rule, where the label colour belongs. A
|
|
52
|
+
* reference whose property cannot be read is reported as `<no property found>`: an
|
|
53
|
+
* unattributable declaration is a hole in the rule, not a pass.
|
|
54
|
+
*/
|
|
55
|
+
export function destructiveColorOffences(source: string): string[] {
|
|
56
|
+
const offences: string[] = [];
|
|
57
|
+
for (const match of source.matchAll(DESTRUCTIVE_REFERENCE)) {
|
|
58
|
+
const property = governingProperty(source, match.index);
|
|
59
|
+
if (property === undefined) {
|
|
60
|
+
offences.push('<no property found>');
|
|
61
|
+
} else if (property === 'color' && !insideDestructiveButtonRule(source, match.index)) {
|
|
62
|
+
offences.push(property);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return offences;
|
|
66
|
+
}
|