@tai42/studio-sdk 25.0.1 → 27.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.
Files changed (262) hide show
  1. package/README.md +143 -26
  2. package/dist/components/action-menu.d.ts +30 -0
  3. package/dist/components/action-menu.d.ts.map +1 -0
  4. package/dist/components/action-menu.js +101 -0
  5. package/dist/components/action-menu.js.map +1 -0
  6. package/dist/components/agent-picker.d.ts +22 -0
  7. package/dist/components/agent-picker.d.ts.map +1 -0
  8. package/dist/components/agent-picker.js +17 -0
  9. package/dist/components/agent-picker.js.map +1 -0
  10. package/dist/components/channel-picker.d.ts +27 -0
  11. package/dist/components/channel-picker.d.ts.map +1 -0
  12. package/dist/components/channel-picker.js +26 -0
  13. package/dist/components/channel-picker.js.map +1 -0
  14. package/dist/components/components.css +226 -8
  15. package/dist/components/confirm-dialog.d.ts +3 -1
  16. package/dist/components/confirm-dialog.d.ts.map +1 -1
  17. package/dist/components/confirm-dialog.js +2 -2
  18. package/dist/components/confirm-dialog.js.map +1 -1
  19. package/dist/components/execution-key-picker.d.ts +26 -10
  20. package/dist/components/execution-key-picker.d.ts.map +1 -1
  21. package/dist/components/execution-key-picker.js +134 -15
  22. package/dist/components/execution-key-picker.js.map +1 -1
  23. package/dist/components/extension-combo-builder.d.ts +7 -7
  24. package/dist/components/extension-combo-builder.d.ts.map +1 -1
  25. package/dist/components/extension-combo-builder.js +42 -16
  26. package/dist/components/extension-combo-builder.js.map +1 -1
  27. package/dist/components/extension-combo-parts.d.ts +13 -11
  28. package/dist/components/extension-combo-parts.d.ts.map +1 -1
  29. package/dist/components/extension-combo-parts.js +10 -19
  30. package/dist/components/extension-combo-parts.js.map +1 -1
  31. package/dist/components/extension-config-editor.d.ts +33 -0
  32. package/dist/components/extension-config-editor.d.ts.map +1 -0
  33. package/dist/components/extension-config-editor.js +190 -0
  34. package/dist/components/extension-config-editor.js.map +1 -0
  35. package/dist/components/extension-grouping.d.ts +4 -41
  36. package/dist/components/extension-grouping.d.ts.map +1 -1
  37. package/dist/components/extension-grouping.js +1 -48
  38. package/dist/components/extension-grouping.js.map +1 -1
  39. package/dist/components/extension-picker.d.ts +4 -3
  40. package/dist/components/extension-picker.d.ts.map +1 -1
  41. package/dist/components/extension-picker.js +6 -6
  42. package/dist/components/extension-picker.js.map +1 -1
  43. package/dist/components/fleet-report.d.ts +8 -3
  44. package/dist/components/fleet-report.d.ts.map +1 -1
  45. package/dist/components/fleet-report.js +12 -2
  46. package/dist/components/fleet-report.js.map +1 -1
  47. package/dist/components/holder-kinds.d.ts +18 -0
  48. package/dist/components/holder-kinds.d.ts.map +1 -0
  49. package/dist/components/holder-kinds.js +41 -0
  50. package/dist/components/holder-kinds.js.map +1 -0
  51. package/dist/components/holder-list.d.ts +58 -0
  52. package/dist/components/holder-list.d.ts.map +1 -0
  53. package/dist/components/holder-list.js +77 -0
  54. package/dist/components/holder-list.js.map +1 -0
  55. package/dist/components/icons/index.d.ts +1 -1
  56. package/dist/components/icons/index.d.ts.map +1 -1
  57. package/dist/components/icons/index.js +1 -1
  58. package/dist/components/icons/index.js.map +1 -1
  59. package/dist/components/icons/nav-map.d.ts.map +1 -1
  60. package/dist/components/icons/nav-map.js +1 -3
  61. package/dist/components/icons/nav-map.js.map +1 -1
  62. package/dist/components/icons/nav-marks.d.ts +0 -4
  63. package/dist/components/icons/nav-marks.d.ts.map +1 -1
  64. package/dist/components/icons/nav-marks.js +0 -4
  65. package/dist/components/icons/nav-marks.js.map +1 -1
  66. package/dist/components/named-list-picker.d.ts +39 -0
  67. package/dist/components/named-list-picker.d.ts.map +1 -0
  68. package/dist/components/named-list-picker.js +61 -0
  69. package/dist/components/named-list-picker.js.map +1 -0
  70. package/dist/components/named-list-test-utils.d.ts +23 -0
  71. package/dist/components/named-list-test-utils.d.ts.map +1 -0
  72. package/dist/components/named-list-test-utils.js +33 -0
  73. package/dist/components/named-list-test-utils.js.map +1 -0
  74. package/dist/components/recycle-roll.d.ts +23 -0
  75. package/dist/components/recycle-roll.d.ts.map +1 -0
  76. package/dist/components/recycle-roll.js +47 -0
  77. package/dist/components/recycle-roll.js.map +1 -0
  78. package/dist/components/stepper.d.ts +28 -0
  79. package/dist/components/stepper.d.ts.map +1 -0
  80. package/dist/components/stepper.js +33 -0
  81. package/dist/components/stepper.js.map +1 -0
  82. package/dist/components/subject-section.d.ts +2 -1
  83. package/dist/components/subject-section.d.ts.map +1 -1
  84. package/dist/components/subject-section.js +10 -5
  85. package/dist/components/subject-section.js.map +1 -1
  86. package/dist/components/tokens.css +31 -41
  87. package/dist/components/tokens.d.ts +3 -4
  88. package/dist/components/tokens.d.ts.map +1 -1
  89. package/dist/components/tokens.js +5 -13
  90. package/dist/components/tokens.js.map +1 -1
  91. package/dist/components/tool-picker-parts.d.ts +5 -1
  92. package/dist/components/tool-picker-parts.d.ts.map +1 -1
  93. package/dist/components/tool-picker-parts.js +8 -6
  94. package/dist/components/tool-picker-parts.js.map +1 -1
  95. package/dist/components/tool-picker.d.ts +77 -2
  96. package/dist/components/tool-picker.d.ts.map +1 -1
  97. package/dist/components/tool-picker.js +87 -29
  98. package/dist/components/tool-picker.js.map +1 -1
  99. package/dist/components/version-history-panel.d.ts +7 -1
  100. package/dist/components/version-history-panel.d.ts.map +1 -1
  101. package/dist/components/version-history-panel.js +2 -2
  102. package/dist/components/version-history-panel.js.map +1 -1
  103. package/dist/components/version-history-rows.d.ts +4 -2
  104. package/dist/components/version-history-rows.d.ts.map +1 -1
  105. package/dist/components/version-history-rows.js +14 -2
  106. package/dist/components/version-history-rows.js.map +1 -1
  107. package/dist/errors.d.ts +5 -0
  108. package/dist/errors.d.ts.map +1 -1
  109. package/dist/errors.js +7 -0
  110. package/dist/errors.js.map +1 -1
  111. package/dist/hooks/useApi.d.ts +524 -197
  112. package/dist/hooks/useApi.d.ts.map +1 -1
  113. package/dist/hooks/useCapabilities.d.ts +43 -10
  114. package/dist/hooks/useCapabilities.d.ts.map +1 -1
  115. package/dist/hooks/useCapabilities.js +39 -10
  116. package/dist/hooks/useCapabilities.js.map +1 -1
  117. package/dist/index.d.ts +25 -12
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +14 -6
  120. package/dist/index.js.map +1 -1
  121. package/dist/marketplace-install/env-var-fields.d.ts +17 -0
  122. package/dist/marketplace-install/env-var-fields.d.ts.map +1 -0
  123. package/dist/marketplace-install/env-var-fields.js +22 -0
  124. package/dist/marketplace-install/env-var-fields.js.map +1 -0
  125. package/dist/marketplace-install/index.d.ts +18 -0
  126. package/dist/marketplace-install/index.d.ts.map +1 -0
  127. package/dist/marketplace-install/index.js +8 -0
  128. package/dist/marketplace-install/index.js.map +1 -0
  129. package/dist/marketplace-install/install-env.d.ts +21 -0
  130. package/dist/marketplace-install/install-env.d.ts.map +1 -0
  131. package/dist/marketplace-install/install-env.js +22 -0
  132. package/dist/marketplace-install/install-env.js.map +1 -0
  133. package/dist/marketplace-install/install-mount.d.ts +42 -0
  134. package/dist/marketplace-install/install-mount.d.ts.map +1 -0
  135. package/dist/marketplace-install/install-mount.js +62 -0
  136. package/dist/marketplace-install/install-mount.js.map +1 -0
  137. package/dist/marketplace-install/install-report.d.ts +14 -0
  138. package/dist/marketplace-install/install-report.d.ts.map +1 -0
  139. package/dist/marketplace-install/install-report.js +19 -0
  140. package/dist/marketplace-install/install-report.js.map +1 -0
  141. package/dist/marketplace-install/install-shell.d.ts +34 -0
  142. package/dist/marketplace-install/install-shell.d.ts.map +1 -0
  143. package/dist/marketplace-install/install-shell.js +32 -0
  144. package/dist/marketplace-install/install-shell.js.map +1 -0
  145. package/dist/marketplace-install/mount-install.d.ts +41 -0
  146. package/dist/marketplace-install/mount-install.d.ts.map +1 -0
  147. package/dist/marketplace-install/mount-install.js +90 -0
  148. package/dist/marketplace-install/mount-install.js.map +1 -0
  149. package/dist/marketplace-install/plugin-detail.d.ts +17 -0
  150. package/dist/marketplace-install/plugin-detail.d.ts.map +1 -0
  151. package/dist/marketplace-install/plugin-detail.js +41 -0
  152. package/dist/marketplace-install/plugin-detail.js.map +1 -0
  153. package/dist/marketplace-install/plugin-install.d.ts +27 -0
  154. package/dist/marketplace-install/plugin-install.d.ts.map +1 -0
  155. package/dist/marketplace-install/plugin-install.js +89 -0
  156. package/dist/marketplace-install/plugin-install.js.map +1 -0
  157. package/dist/marketplace-install/route-items.d.ts +14 -0
  158. package/dist/marketplace-install/route-items.d.ts.map +1 -0
  159. package/dist/marketplace-install/route-items.js +11 -0
  160. package/dist/marketplace-install/route-items.js.map +1 -0
  161. package/dist/marketplace-install/use-debounced-value.d.ts +6 -0
  162. package/dist/marketplace-install/use-debounced-value.d.ts.map +1 -0
  163. package/dist/marketplace-install/use-debounced-value.js +18 -0
  164. package/dist/marketplace-install/use-debounced-value.js.map +1 -0
  165. package/dist/marketplace-install/use-install-action.d.ts +26 -0
  166. package/dist/marketplace-install/use-install-action.d.ts.map +1 -0
  167. package/dist/marketplace-install/use-install-action.js +35 -0
  168. package/dist/marketplace-install/use-install-action.js.map +1 -0
  169. package/dist/marketplace-install/use-install-preview.d.ts +33 -0
  170. package/dist/marketplace-install/use-install-preview.d.ts.map +1 -0
  171. package/dist/marketplace-install/use-install-preview.js +44 -0
  172. package/dist/marketplace-install/use-install-preview.js.map +1 -0
  173. package/dist/marketplace-install/use-mount-install.d.ts +80 -0
  174. package/dist/marketplace-install/use-mount-install.d.ts.map +1 -0
  175. package/dist/marketplace-install/use-mount-install.js +75 -0
  176. package/dist/marketplace-install/use-mount-install.js.map +1 -0
  177. package/dist/navigation/index.d.ts +1 -1
  178. package/dist/navigation/index.d.ts.map +1 -1
  179. package/dist/navigation/index.js.map +1 -1
  180. package/dist/navigation/types.d.ts +23 -7
  181. package/dist/navigation/types.d.ts.map +1 -1
  182. package/dist/plugin/registry.d.ts +0 -8
  183. package/dist/plugin/registry.d.ts.map +1 -1
  184. package/dist/plugin/registry.js +25 -1
  185. package/dist/plugin/registry.js.map +1 -1
  186. package/dist/plugin/types.d.ts +36 -12
  187. package/dist/plugin/types.d.ts.map +1 -1
  188. package/dist/plugin/version.d.ts +6 -4
  189. package/dist/plugin/version.d.ts.map +1 -1
  190. package/dist/plugin/version.js +6 -4
  191. package/dist/plugin/version.js.map +1 -1
  192. package/dist/query-keys.d.ts +45 -4
  193. package/dist/query-keys.d.ts.map +1 -1
  194. package/dist/query-keys.js +52 -4
  195. package/dist/query-keys.js.map +1 -1
  196. package/dist/schema-form/json-schema-meta.d.ts +3 -0
  197. package/dist/schema-form/json-schema-meta.d.ts.map +1 -0
  198. package/dist/schema-form/json-schema-meta.js +3 -0
  199. package/dist/schema-form/json-schema-meta.js.map +1 -0
  200. package/dist/testing.d.ts +2 -0
  201. package/dist/testing.d.ts.map +1 -1
  202. package/dist/testing.js +2 -0
  203. package/dist/testing.js.map +1 -1
  204. package/dist/token-rules.d.ts +26 -0
  205. package/dist/token-rules.d.ts.map +1 -0
  206. package/dist/token-rules.js +64 -0
  207. package/dist/token-rules.js.map +1 -0
  208. package/package.json +2 -2
  209. package/src/components/action-menu.tsx +160 -0
  210. package/src/components/agent-picker.tsx +53 -0
  211. package/src/components/channel-picker.tsx +68 -0
  212. package/src/components/components.css +226 -8
  213. package/src/components/confirm-dialog.tsx +4 -1
  214. package/src/components/execution-key-picker.tsx +252 -36
  215. package/src/components/extension-combo-builder.tsx +61 -29
  216. package/src/components/extension-combo-parts.tsx +28 -38
  217. package/src/components/extension-config-editor.tsx +307 -0
  218. package/src/components/extension-grouping.ts +4 -73
  219. package/src/components/extension-picker.tsx +13 -13
  220. package/src/components/fleet-report.tsx +38 -4
  221. package/src/components/holder-kinds.ts +60 -0
  222. package/src/components/holder-list.tsx +163 -0
  223. package/src/components/icons/index.ts +0 -2
  224. package/src/components/icons/nav-map.ts +0 -4
  225. package/src/components/icons/nav-marks.tsx +0 -15
  226. package/src/components/named-list-picker.tsx +182 -0
  227. package/src/components/named-list-test-utils.tsx +47 -0
  228. package/src/components/recycle-roll.tsx +120 -0
  229. package/src/components/stepper.tsx +111 -0
  230. package/src/components/subject-section.tsx +10 -5
  231. package/src/components/tokens.css +31 -41
  232. package/src/components/tokens.ts +5 -13
  233. package/src/components/tool-picker-parts.tsx +17 -6
  234. package/src/components/tool-picker.tsx +215 -41
  235. package/src/components/version-history-panel.tsx +8 -0
  236. package/src/components/version-history-rows.tsx +14 -2
  237. package/src/errors.ts +10 -0
  238. package/src/hooks/useCapabilities.ts +59 -10
  239. package/src/index.ts +54 -14
  240. package/src/marketplace-install/env-var-fields.tsx +59 -0
  241. package/src/marketplace-install/index.ts +17 -0
  242. package/src/marketplace-install/install-env.ts +39 -0
  243. package/src/marketplace-install/install-mount.ts +96 -0
  244. package/src/marketplace-install/install-report.tsx +54 -0
  245. package/src/marketplace-install/install-shell.tsx +99 -0
  246. package/src/marketplace-install/mount-install.tsx +290 -0
  247. package/src/marketplace-install/plugin-detail.ts +60 -0
  248. package/src/marketplace-install/plugin-install.tsx +221 -0
  249. package/src/marketplace-install/route-items.ts +20 -0
  250. package/src/marketplace-install/use-debounced-value.ts +18 -0
  251. package/src/marketplace-install/use-install-action.ts +60 -0
  252. package/src/marketplace-install/use-install-preview.ts +74 -0
  253. package/src/marketplace-install/use-mount-install.ts +116 -0
  254. package/src/navigation/index.ts +1 -0
  255. package/src/navigation/types.ts +27 -7
  256. package/src/plugin/registry.ts +19 -1
  257. package/src/plugin/types.ts +40 -12
  258. package/src/plugin/version.ts +6 -4
  259. package/src/query-keys.ts +71 -4
  260. package/src/schema-form/json-schema-meta.ts +2 -0
  261. package/src/testing.ts +2 -0
  262. 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>;
@@ -14,6 +14,7 @@ export type {
14
14
  NavigationContextValue,
15
15
  NavigationGuardHandler,
16
16
  PageProps,
17
+ PluginHolderTarget,
17
18
  PluginNavigateOptions,
18
19
  PluginSearch,
19
20
  RouteSearch,
@@ -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
- presets: { preset?: string };
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
- hooks: Record<string, never>;
62
- templates: { template?: string; q?: string };
63
- storage: { q?: string };
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
- scheduling: Record<string, never>;
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
  /**
@@ -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
  }
@@ -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. A named core section groups the
146
- * entry there, after that section's core rows. An absent field or any value that
147
- * is not a live core section renders the entry in the shared "Plugins" section that
148
- * follows the core sections (alongside every other undeclared plugin entry),
149
- * placed after the core sections. The shell tolerates an unknown value at runtime
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' | 'Triggers' | 'Activity' | 'Administration';
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}); absent ⇒ the
166
- * shared "Plugins" section. Additive — an older bundle omits it and stays
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
@@ -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
- * OR the public prop/type surface of any DS component the SDK exports (a prop
6
- * rename/removal bumps it; genuinely internal/visual changes do not).
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 = 1;
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 extensions feature keys its
18
- * read-only catalog on it; a tool's extension-combo save (in the tools feature)
19
- * creates or tears down branch tools, so it invalidates this key too — a
20
- * cross-feature reference that must resolve to one authoritative constant.
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;
@@ -0,0 +1,2 @@
1
+ /** The draft 2020-12 meta-schema URI: a node whose `$ref` is this value holds a JSON Schema. */
2
+ export const JSON_SCHEMA_META_URI = 'https://json-schema.org/draft/2020-12/schema';
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
+ }