@tai42/studio-sdk 25.0.1 → 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.
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,290 @@
1
+ /**
2
+ * The route-mounting install / update form: a base prefix per route-carrying item, a
3
+ * live preview of the resulting absolute paths, loud collision blocking, explicit
4
+ * acceptance of the routes served without authentication, and — on install — the
5
+ * preview's missing env collected in the same form. `MountInstallForm` renders it in
6
+ * place; `MountInstallDialog` renders it in a dialog.
7
+ */
8
+ import type { MarketplaceInstallPreview, MarketplaceInstallResult } from '@tai42/api-client';
9
+ import { type ReactNode, useRef } from 'react';
10
+
11
+ import { Badge } from '../components/badge';
12
+ import { Checkbox } from '../components/checkbox';
13
+ import { Field } from '../components/field';
14
+ import { FormDialog } from '../components/form-dialog';
15
+ import { XCircleIcon } from '../components/icons';
16
+ import { TextInput } from '../components/inputs';
17
+ import { ErrorState, Skeleton } from '../components/primitives';
18
+ import { EnvVarFields } from './env-var-fields';
19
+ import { InstallShell, storeOffNote } from './install-shell';
20
+ import type { RouteItem } from './route-items';
21
+ import { useInstallAction } from './use-install-action';
22
+ import { type MountInstallModel, useMountInstall } from './use-mount-install';
23
+
24
+ export interface MountInstallDialogProps {
25
+ readonly refValue: string;
26
+ readonly version: string | null;
27
+ readonly verb: 'Install' | 'Update';
28
+ readonly routeItems: readonly RouteItem[];
29
+ /**
30
+ * On UPDATE, the installed row's stored `{item_name: base}` — the CURRENT mount each
31
+ * base input seeds from (declared default only for an item with no stored mount).
32
+ * Absent on INSTALL, where every input seeds from the declared default.
33
+ */
34
+ readonly storedMounts?: Record<string, string>;
35
+ /** The server's per-var secret-ness: a toggle for a `true` var is locked on. */
36
+ readonly requiredEnvSecret?: Record<string, boolean>;
37
+ /** Per-var "which item needs it" hint. */
38
+ readonly envHints?: Record<string, string>;
39
+ /** After a 2xx install or update. */
40
+ readonly onDone: (result: MarketplaceInstallResult) => void;
41
+ /** After a failed call, already rendered in place. */
42
+ readonly onFailed?: (error: unknown) => void;
43
+ /** The user closed or cancelled without a completed call. */
44
+ readonly onDismiss: () => void;
45
+ }
46
+
47
+ /** The resolved routes for one item, from the live preview. */
48
+ function ItemRoutes({
49
+ item,
50
+ preview,
51
+ }: {
52
+ readonly item: RouteItem;
53
+ readonly preview: MarketplaceInstallPreview | undefined;
54
+ }): ReactNode {
55
+ if (preview === undefined) return <Skeleton height={48} />;
56
+ const resolved = preview.items.find((row) => row.item === item.name);
57
+ if (resolved === undefined) return null;
58
+ return (
59
+ <ul style={{ margin: 0, paddingLeft: 'var(--tai-space-4)' }}>
60
+ {resolved.routes.map((route) => (
61
+ <li key={`${route.path}/${route.methods.join(',')}`}>
62
+ <code className="tai-mono">{route.full_path}</code>{' '}
63
+ <span className="tai-muted">{route.methods.join(', ')}</span>{' '}
64
+ {route.public ? <Badge variant="warning">public</Badge> : null}
65
+ </li>
66
+ ))}
67
+ </ul>
68
+ );
69
+ }
70
+
71
+ /**
72
+ * The loud collision block: each clashing route, its owner, and the remap remedy.
73
+ * `role="alert"` on the error ground, with its own headline — a collision is a
74
+ * refusal the operator resolves, not a malfunction, so it never wears the generic
75
+ * "something went wrong".
76
+ */
77
+ function CollisionBlock({
78
+ collisions,
79
+ }: {
80
+ readonly collisions: MarketplaceInstallPreview['collisions'];
81
+ }): ReactNode {
82
+ return (
83
+ <div role="alert" className="tai-error-state tai-stack tai-stack-2">
84
+ <strong className="tai-error-state-title">
85
+ <XCircleIcon />
86
+ Route collision
87
+ </strong>
88
+ <p style={{ margin: 0 }}>
89
+ These routes already have an owner. Remap the base above until they no longer clash.
90
+ </p>
91
+ <ul style={{ margin: 0, paddingLeft: 'var(--tai-space-4)' }}>
92
+ {collisions.map((collision) => (
93
+ <li key={`${collision.item}/${collision.full_path}/${collision.methods.join(',')}`}>
94
+ <code className="tai-mono">{collision.full_path}</code>{' '}
95
+ <span className="tai-muted">{collision.methods.join(', ')}</span> — owned by{' '}
96
+ <strong>{collision.conflict_owner}</strong>
97
+ </li>
98
+ ))}
99
+ </ul>
100
+ </div>
101
+ );
102
+ }
103
+
104
+ /**
105
+ * The public-route acceptance block: it names every route that will answer WITHOUT
106
+ * authentication and gates submit on an explicit checkbox.
107
+ */
108
+ function PublicAcceptance({
109
+ rows,
110
+ accepted,
111
+ onAccept,
112
+ }: {
113
+ readonly rows: MarketplaceInstallPreview['new_public_routes'];
114
+ readonly accepted: boolean;
115
+ readonly onAccept: (checked: boolean) => void;
116
+ }): ReactNode {
117
+ return (
118
+ <div role="alert" className="tai-warn-state tai-stack tai-stack-2">
119
+ <strong className="tai-status-warn">Public routes</strong>
120
+ <p style={{ margin: 0 }}>
121
+ These routes are served WITHOUT API-key authentication — anyone who can reach the server can
122
+ call them.
123
+ </p>
124
+ <ul style={{ margin: 0, paddingLeft: 'var(--tai-space-4)' }}>
125
+ {rows.map((row) => (
126
+ <li key={`${row.item}/${row.full_path}/${row.methods.join(',')}`}>
127
+ <code className="tai-mono">{row.full_path}</code>{' '}
128
+ <span className="tai-muted">{row.methods.join(', ')}</span>
129
+ </li>
130
+ ))}
131
+ </ul>
132
+ <Checkbox
133
+ checked={accepted}
134
+ onCheckedChange={onAccept}
135
+ label="I accept these routes are served without authentication"
136
+ />
137
+ </div>
138
+ );
139
+ }
140
+
141
+ /** The form body both frames render: the bases, the preview, and the env to collect. */
142
+ function MountInstallFields({
143
+ model,
144
+ refValue,
145
+ version,
146
+ verb,
147
+ routeItems,
148
+ requiredEnvSecret,
149
+ envHints,
150
+ }: {
151
+ readonly model: MountInstallModel;
152
+ readonly refValue: string;
153
+ readonly version: string | null;
154
+ readonly verb: 'Install' | 'Update';
155
+ readonly routeItems: readonly RouteItem[];
156
+ readonly requiredEnvSecret?: Record<string, boolean>;
157
+ readonly envHints?: Record<string, string>;
158
+ }): ReactNode {
159
+ return (
160
+ <div className="tai-stack tai-stack-3">
161
+ <p style={{ margin: 0 }}>
162
+ {verb} {refValue}
163
+ {version !== null ? ` v${version}` : ''}? Its routes mount under the bases below. Remap a
164
+ base to move where a route answers.
165
+ </p>
166
+ {routeItems.map((item) => (
167
+ <div key={item.name} className="tai-stack tai-stack-2">
168
+ <Field label={`${item.name} base`}>
169
+ <TextInput
170
+ value={model.bases[item.name] ?? ''}
171
+ onChange={(event) => {
172
+ const next = event.currentTarget.value;
173
+ model.setBases((prev) => ({ ...prev, [item.name]: next }));
174
+ }}
175
+ />
176
+ </Field>
177
+ <ItemRoutes item={item} preview={model.preview} />
178
+ </div>
179
+ ))}
180
+ {model.previewIsError ? (
181
+ <ErrorState message="The install preview failed. Fix the error and edit a base to retry." />
182
+ ) : null}
183
+ {model.collisions.length > 0 ? <CollisionBlock collisions={model.collisions} /> : null}
184
+ {model.requiresAccept ? (
185
+ <PublicAcceptance
186
+ rows={model.publicRows}
187
+ accepted={model.accepted}
188
+ onAccept={model.setAccepted}
189
+ />
190
+ ) : null}
191
+ {model.envToCollect.length > 0 ? (
192
+ <EnvVarFields
193
+ requiredVars={model.envToCollect}
194
+ values={model.envValues}
195
+ secret={model.envSecretMap}
196
+ requiredSecret={requiredEnvSecret}
197
+ hints={envHints}
198
+ onChangeValue={(name, value) => {
199
+ model.setEnvValues((prev) => ({ ...prev, [name]: value }));
200
+ }}
201
+ onToggleSecret={(name, checked) => {
202
+ model.setEnvSecretOverride((prev) => ({ ...prev, [name]: checked }));
203
+ }}
204
+ />
205
+ ) : null}
206
+ </div>
207
+ );
208
+ }
209
+
210
+ /** The mount form in place, with its own Cancel and submit buttons. */
211
+ export function MountInstallForm(props: MountInstallDialogProps): ReactNode {
212
+ const { refValue, version, verb, routeItems, requiredEnvSecret, envHints } = props;
213
+ const model = useMountInstall(props);
214
+ const action = useInstallAction(refValue, verb);
215
+ const failure = action.state.status === 'failed' ? action.state.error : null;
216
+ return (
217
+ <InstallShell
218
+ layout="panel"
219
+ title={`${verb} plugin`}
220
+ submitLabel={verb}
221
+ pendingLabel={verb === 'Install' ? 'Installing' : 'Updating'}
222
+ isPending={action.state.status === 'pending'}
223
+ error={storeOffNote(failure) === undefined ? failure : null}
224
+ disabledNote={storeOffNote(failure)}
225
+ submitDisabled={model.blocked}
226
+ onSubmit={() => {
227
+ action.run(model.extras()).then(props.onDone, (error: unknown) => {
228
+ props.onFailed?.(error);
229
+ });
230
+ }}
231
+ onDismiss={props.onDismiss}
232
+ >
233
+ <MountInstallFields
234
+ model={model}
235
+ refValue={refValue}
236
+ version={version}
237
+ verb={verb}
238
+ routeItems={routeItems}
239
+ requiredEnvSecret={requiredEnvSecret}
240
+ envHints={envHints}
241
+ />
242
+ </InstallShell>
243
+ );
244
+ }
245
+
246
+ /**
247
+ * The mount form in a dialog. It previews the resolved routes on open and on every
248
+ * (debounced) base edit, blocks submit on a collision or an unaccepted public route,
249
+ * and closes only after a 2xx; a failure renders in the dialog and it stays open.
250
+ */
251
+ export function MountInstallDialog(props: MountInstallDialogProps): ReactNode {
252
+ const { refValue, version, verb, routeItems, requiredEnvSecret, envHints } = props;
253
+ const model = useMountInstall(props);
254
+ const action = useInstallAction(refValue, verb);
255
+ // The dialog's own close after a 2xx is not a dismissal.
256
+ const doneRef = useRef(false);
257
+ const submit = async (): Promise<void> => {
258
+ let result: MarketplaceInstallResult;
259
+ try {
260
+ result = await action.run(model.extras());
261
+ } catch (error: unknown) {
262
+ props.onFailed?.(error);
263
+ throw error;
264
+ }
265
+ doneRef.current = true;
266
+ props.onDone(result);
267
+ };
268
+ return (
269
+ <FormDialog
270
+ title={`${verb} plugin`}
271
+ submitLabel={verb}
272
+ pendingLabel={verb === 'Install' ? 'Installing' : 'Updating'}
273
+ submitDisabled={model.blocked}
274
+ onSubmit={submit}
275
+ onClose={() => {
276
+ if (!doneRef.current) props.onDismiss();
277
+ }}
278
+ >
279
+ <MountInstallFields
280
+ model={model}
281
+ refValue={refValue}
282
+ version={version}
283
+ verb={verb}
284
+ routeItems={routeItems}
285
+ requiredEnvSecret={requiredEnvSecret}
286
+ envHints={envHints}
287
+ />
288
+ </FormDialog>
289
+ );
290
+ }
@@ -0,0 +1,60 @@
1
+ /** Pure readings of a marketplace plugin's detail that the install forms seed from. */
2
+ import type { MarketplaceDelivery, MarketplacePluginDetail } from '@tai42/api-client';
3
+
4
+ /**
5
+ * How the listing's latest version is delivered, derived exactly the way the server
6
+ * derives it — from whether the spec names a package. The registry's plugin-detail
7
+ * body carries no `delivery` field (that is server-computed and rides the installed
8
+ * rows and the install preview), so the detail is read off the listing's `package`:
9
+ * `null` (a descriptor listing that names none) ⇒ `descriptor`, else `package`.
10
+ */
11
+ export function deliveryOf(detail: MarketplacePluginDetail): MarketplaceDelivery {
12
+ return detail.package === null ? 'descriptor' : 'package';
13
+ }
14
+
15
+ /**
16
+ * The env picture a listing's items declare, folded across the detail: each
17
+ * required var's derived `secret`-ness and the item(s) that need it. This is a
18
+ * SUPPLEMENTARY fallback only — the registry's plugin-detail body carries no per-item
19
+ * required_env, so in production this is empty; the install PREVIEW is the authority.
20
+ */
21
+ interface RequiredEnvEntry {
22
+ readonly secret: boolean;
23
+ readonly items: string[];
24
+ }
25
+
26
+ function requiredEnvIndex(detail: MarketplacePluginDetail): Map<string, RequiredEnvEntry> {
27
+ const index = new Map<string, RequiredEnvEntry>();
28
+ for (const item of detail.latest?.items ?? []) {
29
+ // The registry omits per-item required_env (undefined); only a fixture/preview
30
+ // that declares it drives these pre-dialog hints.
31
+ for (const req of item.required_env ?? []) {
32
+ const entry = index.get(req.name);
33
+ if (entry === undefined) {
34
+ index.set(req.name, { secret: req.secret, items: [item.name] });
35
+ } else {
36
+ index.set(req.name, {
37
+ secret: entry.secret || req.secret,
38
+ items: [...entry.items, item.name],
39
+ });
40
+ }
41
+ }
42
+ }
43
+ return index;
44
+ }
45
+
46
+ export interface RequiredEnvHints {
47
+ readonly requiredEnvSecret: Record<string, boolean>;
48
+ readonly requiredEnvHints: Record<string, string>;
49
+ }
50
+
51
+ /** The detail's declared env folded into the secret band and per-var hint the forms seed from. */
52
+ export function requiredEnvFromDetail(detail: MarketplacePluginDetail): RequiredEnvHints {
53
+ const requiredEnvSecret: Record<string, boolean> = {};
54
+ const requiredEnvHints: Record<string, string> = {};
55
+ for (const [envName, entry] of requiredEnvIndex(detail)) {
56
+ requiredEnvSecret[envName] = entry.secret;
57
+ requiredEnvHints[envName] = entry.items.join(', ');
58
+ }
59
+ return { requiredEnvSecret, requiredEnvHints };
60
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * The install of one marketplace plugin, in place (`PluginInstallPanel`) or in a
3
+ * dialog (`PluginInstallDialog`). A plugin with route-carrying items installs through
4
+ * the mount form, which also collects the preview's missing env; any other plugin
5
+ * previews once and either collects the env the preview names or shows a plain
6
+ * confirm.
7
+ */
8
+ import type {
9
+ MarketplaceInstallBody,
10
+ MarketplaceInstallResult,
11
+ MarketplacePluginDetail,
12
+ } from '@tai42/api-client';
13
+ import { type ReactNode, useMemo, useState } from 'react';
14
+
15
+ import { Skeleton } from '../components/primitives';
16
+ import { EnvVarFields } from './env-var-fields';
17
+ import { installEnvBands } from './install-env';
18
+ import { collectEnv } from './install-mount';
19
+ import { InstallFailure, type InstallLayout, InstallShell, storeOffNote } from './install-shell';
20
+ import { MountInstallDialog, MountInstallForm } from './mount-install';
21
+ import { deliveryOf, requiredEnvFromDetail } from './plugin-detail';
22
+ import { routeItemsOf } from './route-items';
23
+ import { useInstallAction } from './use-install-action';
24
+ import { type InstallPreviewState, useInstallPreview } from './use-install-preview';
25
+
26
+ export interface PluginInstallProps {
27
+ readonly refValue: string;
28
+ readonly version: string | null;
29
+ readonly detail: MarketplacePluginDetail;
30
+ /** After a 2xx install; the host invalidates its own caches and renders the report. */
31
+ readonly onInstalled: (result: MarketplaceInstallResult) => void;
32
+ /** After a failed install, already rendered in place; the host may record a store-off refusal. */
33
+ readonly onFailed?: (error: unknown) => void;
34
+ /** The user closed or cancelled without installing. */
35
+ readonly onDismiss: () => void;
36
+ }
37
+
38
+ /** The env-collecting body: one value per var the deployment does not already provide. */
39
+ function EnvInstallFields({
40
+ refValue,
41
+ version,
42
+ missingVars,
43
+ values,
44
+ secretMap,
45
+ lockedSecret,
46
+ envHints,
47
+ onChangeValue,
48
+ onToggleSecret,
49
+ }: {
50
+ readonly refValue: string;
51
+ readonly version: string | null;
52
+ readonly missingVars: readonly string[];
53
+ readonly values: Record<string, string>;
54
+ readonly secretMap: Record<string, boolean>;
55
+ readonly lockedSecret: Record<string, boolean>;
56
+ readonly envHints: Record<string, string>;
57
+ readonly onChangeValue: (name: string, value: string) => void;
58
+ readonly onToggleSecret: (name: string, checked: boolean) => void;
59
+ }): ReactNode {
60
+ return (
61
+ <div className="tai-stack tai-stack-3">
62
+ <p style={{ margin: 0 }}>
63
+ Install {refValue}
64
+ {version !== null ? ` v${version}` : ''}?
65
+ {missingVars.length > 0
66
+ ? ' This plugin needs these values to install. Leave a field blank if the deployment already provides it.'
67
+ : ''}
68
+ </p>
69
+ {missingVars.length > 0 ? (
70
+ <EnvVarFields
71
+ requiredVars={missingVars}
72
+ values={values}
73
+ secret={secretMap}
74
+ requiredSecret={lockedSecret}
75
+ hints={envHints}
76
+ onChangeValue={onChangeValue}
77
+ onToggleSecret={onToggleSecret}
78
+ />
79
+ ) : null}
80
+ </div>
81
+ );
82
+ }
83
+
84
+ /**
85
+ * The install of a plugin with no route-carrying item. Whether it needs install-time
86
+ * env is the install PREVIEW's call — the server authority — because the registry's
87
+ * plugin-detail body carries no per-item required_env. A preview naming required or
88
+ * missing env collects one value per missing var (a secret var's toggle is locked
89
+ * on, a free var starts off; a blank field is omitted, the deployment may still
90
+ * provide it). A clean no-env preview shows the plain confirm. The submit stays
91
+ * blocked while the preview loads or after it failed: the env picture is unverified.
92
+ */
93
+ function NonRouteInstall({
94
+ layout,
95
+ refValue,
96
+ version,
97
+ detail,
98
+ onInstalled,
99
+ onFailed,
100
+ onDismiss,
101
+ }: PluginInstallProps & { readonly layout: InstallLayout }): ReactNode {
102
+ const request = useMemo(() => ({ ref: refValue, version }), [refValue, version]);
103
+ const { state: previewState } = useInstallPreview(request);
104
+ const action = useInstallAction(refValue, 'Install');
105
+ const { requiredEnvSecret, requiredEnvHints } = requiredEnvFromDetail(detail);
106
+ const [values, setValues] = useState<Record<string, string>>({});
107
+ const [secretOverride, setSecretOverride] = useState<Record<string, boolean>>({});
108
+
109
+ const preview = previewState.status === 'ready' ? previewState.preview : null;
110
+ const needsEnv =
111
+ preview !== null && (preview.required_env.length > 0 || preview.missing_env.length > 0);
112
+ const missingVars = preview?.missing_env ?? [];
113
+ // The secret band merges the preview's per-var authority, the detail-derived
114
+ // fallback, and the operator override; a preview/detail-secret var is LOCKED on.
115
+ const { secretMap, lockedSecret } = installEnvBands(
116
+ missingVars,
117
+ preview?.required_env ?? [],
118
+ requiredEnvSecret,
119
+ secretOverride,
120
+ );
121
+
122
+ const failure = action.state.status === 'failed' ? action.state.error : null;
123
+ const submit = (): void => {
124
+ let body: Omit<MarketplaceInstallBody, 'ref'> = {};
125
+ if (needsEnv) {
126
+ const { env, secretKeys } = collectEnv(missingVars, values, secretMap);
127
+ // Every var pre-satisfied → a plain install body.
128
+ if (Object.keys(env).length > 0) body = { env, secret_keys: secretKeys };
129
+ }
130
+ action.run(body).then(onInstalled, (error: unknown) => {
131
+ onFailed?.(error);
132
+ });
133
+ };
134
+
135
+ return (
136
+ <InstallShell
137
+ layout={layout}
138
+ title="Install plugin"
139
+ submitLabel="Install"
140
+ pendingLabel="Installing"
141
+ isPending={action.state.status === 'pending'}
142
+ error={storeOffNote(failure) === undefined ? failure : null}
143
+ disabledNote={previewNote(previewState) ?? storeOffNote(failure)}
144
+ onSubmit={submit}
145
+ onDismiss={onDismiss}
146
+ >
147
+ {needsEnv ? (
148
+ <EnvInstallFields
149
+ refValue={refValue}
150
+ version={version}
151
+ missingVars={missingVars}
152
+ values={values}
153
+ secretMap={secretMap}
154
+ lockedSecret={lockedSecret}
155
+ envHints={requiredEnvHints}
156
+ onChangeValue={(name, value) => {
157
+ setValues((prev) => ({ ...prev, [name]: value }));
158
+ }}
159
+ onToggleSecret={(name, checked) => {
160
+ setSecretOverride((prev) => ({ ...prev, [name]: checked }));
161
+ }}
162
+ />
163
+ ) : (
164
+ <p style={{ margin: 0 }}>
165
+ Install {refValue}
166
+ {detail.latest !== null ? ` v${detail.latest.version}` : ''}?{' '}
167
+ {/* A descriptor plugin installs nothing but its manifest entry; a packaged
168
+ plugin is pip-installed. */}
169
+ {deliveryOf(detail) === 'descriptor'
170
+ ? 'The app will register this plugin and reload.'
171
+ : 'The app will pip-install the package and reload.'}
172
+ </p>
173
+ )}
174
+ </InstallShell>
175
+ );
176
+ }
177
+
178
+ /** What the preview puts in the failure slot: a skeleton while it loads, its failure after. */
179
+ function previewNote(state: InstallPreviewState): ReactNode {
180
+ if (state.status === 'loading') return <Skeleton height={48} />;
181
+ if (state.status === 'failed') return <InstallFailure error={state.error} />;
182
+ return undefined;
183
+ }
184
+
185
+ function PluginInstall({
186
+ layout,
187
+ ...props
188
+ }: PluginInstallProps & { readonly layout: InstallLayout }): ReactNode {
189
+ const { refValue, version, detail, onInstalled, onFailed, onDismiss } = props;
190
+ const routeItems = useMemo(() => routeItemsOf(detail.latest?.items ?? []), [detail]);
191
+ if (routeItems.length === 0) return <NonRouteInstall layout={layout} {...props} />;
192
+ const { requiredEnvSecret, requiredEnvHints } = requiredEnvFromDetail(detail);
193
+ const Mount = layout === 'dialog' ? MountInstallDialog : MountInstallForm;
194
+ return (
195
+ <Mount
196
+ refValue={refValue}
197
+ version={version}
198
+ verb="Install"
199
+ routeItems={routeItems}
200
+ requiredEnvSecret={requiredEnvSecret}
201
+ envHints={requiredEnvHints}
202
+ onDone={onInstalled}
203
+ onFailed={onFailed}
204
+ onDismiss={onDismiss}
205
+ />
206
+ );
207
+ }
208
+
209
+ /** The install in place, with its own Install button. */
210
+ export function PluginInstallPanel(props: PluginInstallProps): ReactNode {
211
+ return <PluginInstall layout="panel" {...props} />;
212
+ }
213
+
214
+ /** The install in a dialog titled "Install plugin". */
215
+ export function PluginInstallDialog(props: PluginInstallProps): ReactNode {
216
+ return <PluginInstall layout="dialog" {...props} />;
217
+ }
218
+
219
+ /** The line that stands in for an Install control this session's grant does not admit. */
220
+ export const INSTALL_NO_GRANT =
221
+ 'This session cannot install from the marketplace. Installing needs administrator access to POST /api/marketplace/install; ask an administrator to install it.';
@@ -0,0 +1,20 @@
1
+ import type { MarketplaceRoutesDecl } from '@tai42/api-client';
2
+
3
+ /** A route-carrying item as the install form drives it: its name, kind, and declaration. */
4
+ export interface RouteItem {
5
+ readonly name: string;
6
+ readonly kind: string;
7
+ readonly routes: MarketplaceRoutesDecl;
8
+ }
9
+
10
+ /** The route-carrying items of a published version, in declaration order. */
11
+ export function routeItemsOf(
12
+ items: readonly { kind: string; name: string; routes?: MarketplaceRoutesDecl | null }[],
13
+ ): RouteItem[] {
14
+ const out: RouteItem[] = [];
15
+ for (const item of items) {
16
+ if (item.routes === null || item.routes === undefined) continue;
17
+ out.push({ name: item.name, kind: item.kind, routes: item.routes });
18
+ }
19
+ return out;
20
+ }
@@ -0,0 +1,18 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ /**
4
+ * Track a value that only settles `delayMs` after it last changed. The first
5
+ * value settles immediately, so a read keyed on it fires on mount before any edit.
6
+ */
7
+ export function useDebouncedValue<T>(value: T, delayMs: number): T {
8
+ const [settled, setSettled] = useState(value);
9
+ useEffect(() => {
10
+ const timer = setTimeout(() => {
11
+ setSettled(value);
12
+ }, delayMs);
13
+ return () => {
14
+ clearTimeout(timer);
15
+ };
16
+ }, [value, delayMs]);
17
+ return settled;
18
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The one install / update call path (`POST /api/marketplace/install` or
3
+ * `/update`), as a plain state machine through `useApi`. The ref is closed over;
4
+ * the caller supplies the rest of the body.
5
+ */
6
+ import type { MarketplaceInstallBody, MarketplaceInstallResult } from '@tai42/api-client';
7
+ import { useCallback, useEffect, useRef, useState } from 'react';
8
+
9
+ import { useApi } from '../hooks/useApi';
10
+
11
+ export type InstallActionState =
12
+ | { readonly status: 'idle' }
13
+ | { readonly status: 'pending' }
14
+ | { readonly status: 'done'; readonly result: MarketplaceInstallResult }
15
+ | { readonly status: 'failed'; readonly error: unknown };
16
+
17
+ export interface InstallAction {
18
+ readonly state: InstallActionState;
19
+ /** Resolves with the receipt; rejects with the server error after recording `failed`. */
20
+ readonly run: (body: Omit<MarketplaceInstallBody, 'ref'>) => Promise<MarketplaceInstallResult>;
21
+ /** Back to `idle`. */
22
+ readonly reset: () => void;
23
+ }
24
+
25
+ export function useInstallAction(refValue: string, verb: 'Install' | 'Update'): InstallAction {
26
+ const api = useApi();
27
+ const [state, setState] = useState<InstallActionState>({ status: 'idle' });
28
+ // A reply landing after unmount must not set state.
29
+ const mountedRef = useRef(true);
30
+ useEffect(() => {
31
+ mountedRef.current = true;
32
+ return () => {
33
+ mountedRef.current = false;
34
+ };
35
+ }, []);
36
+
37
+ const run = useCallback(
38
+ async (body: Omit<MarketplaceInstallBody, 'ref'>): Promise<MarketplaceInstallResult> => {
39
+ setState({ status: 'pending' });
40
+ try {
41
+ const result =
42
+ verb === 'Install'
43
+ ? await api.installMarketplacePlugin({ ref: refValue, ...body })
44
+ : await api.updateMarketplacePlugin({ ref: refValue, ...body });
45
+ if (mountedRef.current) setState({ status: 'done', result });
46
+ return result;
47
+ } catch (error: unknown) {
48
+ if (mountedRef.current) setState({ status: 'failed', error });
49
+ throw error;
50
+ }
51
+ },
52
+ [api, refValue, verb],
53
+ );
54
+
55
+ const reset = useCallback(() => {
56
+ setState({ status: 'idle' });
57
+ }, []);
58
+
59
+ return { state, run, reset };
60
+ }