@tai42/studio-sdk 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (422) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +15 -0
  3. package/README.md +318 -0
  4. package/SECURITY.md +51 -0
  5. package/dist/components/badge.d.ts +13 -0
  6. package/dist/components/badge.d.ts.map +1 -0
  7. package/dist/components/badge.js +14 -0
  8. package/dist/components/badge.js.map +1 -0
  9. package/dist/components/checkbox.d.ts +24 -0
  10. package/dist/components/checkbox.d.ts.map +1 -0
  11. package/dist/components/checkbox.js +31 -0
  12. package/dist/components/checkbox.js.map +1 -0
  13. package/dist/components/code-block.d.ts +7 -0
  14. package/dist/components/code-block.d.ts.map +1 -0
  15. package/dist/components/code-block.js +23 -0
  16. package/dist/components/code-block.js.map +1 -0
  17. package/dist/components/completion-input.d.ts +31 -0
  18. package/dist/components/completion-input.d.ts.map +1 -0
  19. package/dist/components/completion-input.js +61 -0
  20. package/dist/components/completion-input.js.map +1 -0
  21. package/dist/components/components.css +2212 -0
  22. package/dist/components/confirm-dialog.d.ts +40 -0
  23. package/dist/components/confirm-dialog.d.ts.map +1 -0
  24. package/dist/components/confirm-dialog.js +11 -0
  25. package/dist/components/confirm-dialog.js.map +1 -0
  26. package/dist/components/control-styles.d.ts +12 -0
  27. package/dist/components/control-styles.d.ts.map +1 -0
  28. package/dist/components/control-styles.js +14 -0
  29. package/dist/components/control-styles.js.map +1 -0
  30. package/dist/components/copy-field.d.ts +8 -0
  31. package/dist/components/copy-field.d.ts.map +1 -0
  32. package/dist/components/copy-field.js +106 -0
  33. package/dist/components/copy-field.js.map +1 -0
  34. package/dist/components/dialog.d.ts +21 -0
  35. package/dist/components/dialog.d.ts.map +1 -0
  36. package/dist/components/dialog.js +25 -0
  37. package/dist/components/dialog.js.map +1 -0
  38. package/dist/components/drawer.d.ts +22 -0
  39. package/dist/components/drawer.d.ts.map +1 -0
  40. package/dist/components/drawer.js +32 -0
  41. package/dist/components/drawer.js.map +1 -0
  42. package/dist/components/error-boundary.d.ts +33 -0
  43. package/dist/components/error-boundary.d.ts.map +1 -0
  44. package/dist/components/error-boundary.js +36 -0
  45. package/dist/components/error-boundary.js.map +1 -0
  46. package/dist/components/extension-combo-builder.d.ts +70 -0
  47. package/dist/components/extension-combo-builder.d.ts.map +1 -0
  48. package/dist/components/extension-combo-builder.js +154 -0
  49. package/dist/components/extension-combo-builder.js.map +1 -0
  50. package/dist/components/extension-grouping.d.ts +65 -0
  51. package/dist/components/extension-grouping.d.ts.map +1 -0
  52. package/dist/components/extension-grouping.js +78 -0
  53. package/dist/components/extension-grouping.js.map +1 -0
  54. package/dist/components/extension-picker.d.ts +35 -0
  55. package/dist/components/extension-picker.d.ts.map +1 -0
  56. package/dist/components/extension-picker.js +31 -0
  57. package/dist/components/extension-picker.js.map +1 -0
  58. package/dist/components/external-link.d.ts +19 -0
  59. package/dist/components/external-link.d.ts.map +1 -0
  60. package/dist/components/external-link.js +12 -0
  61. package/dist/components/external-link.js.map +1 -0
  62. package/dist/components/field.d.ts +64 -0
  63. package/dist/components/field.d.ts.map +1 -0
  64. package/dist/components/field.js +56 -0
  65. package/dist/components/field.js.map +1 -0
  66. package/dist/components/fleet-report.d.ts +39 -0
  67. package/dist/components/fleet-report.d.ts.map +1 -0
  68. package/dist/components/fleet-report.js +50 -0
  69. package/dist/components/fleet-report.js.map +1 -0
  70. package/dist/components/fonts.css +22 -0
  71. package/dist/components/icons.d.ts +142 -0
  72. package/dist/components/icons.d.ts.map +1 -0
  73. package/dist/components/icons.js +139 -0
  74. package/dist/components/icons.js.map +1 -0
  75. package/dist/components/inputs.d.ts +52 -0
  76. package/dist/components/inputs.d.ts.map +1 -0
  77. package/dist/components/inputs.js +35 -0
  78. package/dist/components/inputs.js.map +1 -0
  79. package/dist/components/json-diff.d.ts +40 -0
  80. package/dist/components/json-diff.d.ts.map +1 -0
  81. package/dist/components/json-diff.js +119 -0
  82. package/dist/components/json-diff.js.map +1 -0
  83. package/dist/components/json-tree.d.ts +8 -0
  84. package/dist/components/json-tree.d.ts.map +1 -0
  85. package/dist/components/json-tree.js +70 -0
  86. package/dist/components/json-tree.js.map +1 -0
  87. package/dist/components/modal-focus.d.ts +15 -0
  88. package/dist/components/modal-focus.d.ts.map +1 -0
  89. package/dist/components/modal-focus.js +136 -0
  90. package/dist/components/modal-focus.js.map +1 -0
  91. package/dist/components/page-header.d.ts +56 -0
  92. package/dist/components/page-header.d.ts.map +1 -0
  93. package/dist/components/page-header.js +24 -0
  94. package/dist/components/page-header.js.map +1 -0
  95. package/dist/components/primitives.d.ts +186 -0
  96. package/dist/components/primitives.d.ts.map +1 -0
  97. package/dist/components/primitives.js +204 -0
  98. package/dist/components/primitives.js.map +1 -0
  99. package/dist/components/progress-bar.d.ts +29 -0
  100. package/dist/components/progress-bar.d.ts.map +1 -0
  101. package/dist/components/progress-bar.js +48 -0
  102. package/dist/components/progress-bar.js.map +1 -0
  103. package/dist/components/radio-group.d.ts +37 -0
  104. package/dist/components/radio-group.d.ts.map +1 -0
  105. package/dist/components/radio-group.js +56 -0
  106. package/dist/components/radio-group.js.map +1 -0
  107. package/dist/components/reveal-input.d.ts +13 -0
  108. package/dist/components/reveal-input.d.ts.map +1 -0
  109. package/dist/components/reveal-input.js +31 -0
  110. package/dist/components/reveal-input.js.map +1 -0
  111. package/dist/components/scroll-region.d.ts +85 -0
  112. package/dist/components/scroll-region.d.ts.map +1 -0
  113. package/dist/components/scroll-region.js +574 -0
  114. package/dist/components/scroll-region.js.map +1 -0
  115. package/dist/components/select.d.ts +53 -0
  116. package/dist/components/select.d.ts.map +1 -0
  117. package/dist/components/select.js +33 -0
  118. package/dist/components/select.js.map +1 -0
  119. package/dist/components/table.d.ts +59 -0
  120. package/dist/components/table.d.ts.map +1 -0
  121. package/dist/components/table.js +20 -0
  122. package/dist/components/table.js.map +1 -0
  123. package/dist/components/tabs.d.ts +15 -0
  124. package/dist/components/tabs.d.ts.map +1 -0
  125. package/dist/components/tabs.js +13 -0
  126. package/dist/components/tabs.js.map +1 -0
  127. package/dist/components/tags.d.ts +46 -0
  128. package/dist/components/tags.d.ts.map +1 -0
  129. package/dist/components/tags.js +68 -0
  130. package/dist/components/tags.js.map +1 -0
  131. package/dist/components/tokens.css +392 -0
  132. package/dist/components/tokens.d.ts +15 -0
  133. package/dist/components/tokens.d.ts.map +1 -0
  134. package/dist/components/tokens.js +101 -0
  135. package/dist/components/tokens.js.map +1 -0
  136. package/dist/components/tool-picker.d.ts +24 -0
  137. package/dist/components/tool-picker.d.ts.map +1 -0
  138. package/dist/components/tool-picker.js +97 -0
  139. package/dist/components/tool-picker.js.map +1 -0
  140. package/dist/components/tooltip.d.ts +17 -0
  141. package/dist/components/tooltip.d.ts.map +1 -0
  142. package/dist/components/tooltip.js +18 -0
  143. package/dist/components/tooltip.js.map +1 -0
  144. package/dist/components/version-history-panel.d.ts +71 -0
  145. package/dist/components/version-history-panel.d.ts.map +1 -0
  146. package/dist/components/version-history-panel.js +148 -0
  147. package/dist/components/version-history-panel.js.map +1 -0
  148. package/dist/download.d.ts +7 -0
  149. package/dist/download.d.ts.map +1 -0
  150. package/dist/download.js +18 -0
  151. package/dist/download.js.map +1 -0
  152. package/dist/element-slot.d.ts +4 -0
  153. package/dist/element-slot.d.ts.map +1 -0
  154. package/dist/element-slot.js +20 -0
  155. package/dist/element-slot.js.map +1 -0
  156. package/dist/elicitation/ElicitationForm.d.ts +34 -0
  157. package/dist/elicitation/ElicitationForm.d.ts.map +1 -0
  158. package/dist/elicitation/ElicitationForm.js +24 -0
  159. package/dist/elicitation/ElicitationForm.js.map +1 -0
  160. package/dist/errors.d.ts +9 -0
  161. package/dist/errors.d.ts.map +1 -0
  162. package/dist/errors.js +15 -0
  163. package/dist/errors.js.map +1 -0
  164. package/dist/extension-combos.d.ts +13 -0
  165. package/dist/extension-combos.d.ts.map +1 -0
  166. package/dist/extension-combos.js +9 -0
  167. package/dist/extension-combos.js.map +1 -0
  168. package/dist/hooks/useApi.d.ts +1222 -0
  169. package/dist/hooks/useApi.d.ts.map +1 -0
  170. package/dist/hooks/useApi.js +22 -0
  171. package/dist/hooks/useApi.js.map +1 -0
  172. package/dist/hooks/useAuth.d.ts +13 -0
  173. package/dist/hooks/useAuth.d.ts.map +1 -0
  174. package/dist/hooks/useAuth.js +53 -0
  175. package/dist/hooks/useAuth.js.map +1 -0
  176. package/dist/hooks/useBreakpoint.d.ts +10 -0
  177. package/dist/hooks/useBreakpoint.d.ts.map +1 -0
  178. package/dist/hooks/useBreakpoint.js +66 -0
  179. package/dist/hooks/useBreakpoint.js.map +1 -0
  180. package/dist/hooks/useCapabilities.d.ts +88 -0
  181. package/dist/hooks/useCapabilities.d.ts.map +1 -0
  182. package/dist/hooks/useCapabilities.js +203 -0
  183. package/dist/hooks/useCapabilities.js.map +1 -0
  184. package/dist/hooks/useSse.d.ts +13 -0
  185. package/dist/hooks/useSse.d.ts.map +1 -0
  186. package/dist/hooks/useSse.js +298 -0
  187. package/dist/hooks/useSse.js.map +1 -0
  188. package/dist/hooks/useTheme.d.ts +20 -0
  189. package/dist/hooks/useTheme.d.ts.map +1 -0
  190. package/dist/hooks/useTheme.js +97 -0
  191. package/dist/hooks/useTheme.js.map +1 -0
  192. package/dist/hooks/useUnauthorized.d.ts +3 -0
  193. package/dist/hooks/useUnauthorized.d.ts.map +1 -0
  194. package/dist/hooks/useUnauthorized.js +18 -0
  195. package/dist/hooks/useUnauthorized.js.map +1 -0
  196. package/dist/host.d.ts +11 -0
  197. package/dist/host.d.ts.map +1 -0
  198. package/dist/host.js +10 -0
  199. package/dist/host.js.map +1 -0
  200. package/dist/index.d.ts +118 -0
  201. package/dist/index.d.ts.map +1 -0
  202. package/dist/index.js +91 -0
  203. package/dist/index.js.map +1 -0
  204. package/dist/navigation/context.d.ts +25 -0
  205. package/dist/navigation/context.d.ts.map +1 -0
  206. package/dist/navigation/context.js +45 -0
  207. package/dist/navigation/context.js.map +1 -0
  208. package/dist/navigation/index.d.ts +4 -0
  209. package/dist/navigation/index.d.ts.map +1 -0
  210. package/dist/navigation/index.js +2 -0
  211. package/dist/navigation/index.js.map +1 -0
  212. package/dist/navigation/types.d.ts +97 -0
  213. package/dist/navigation/types.d.ts.map +1 -0
  214. package/dist/navigation/types.js +16 -0
  215. package/dist/navigation/types.js.map +1 -0
  216. package/dist/plugin/host-state.d.ts +45 -0
  217. package/dist/plugin/host-state.d.ts.map +1 -0
  218. package/dist/plugin/host-state.js +62 -0
  219. package/dist/plugin/host-state.js.map +1 -0
  220. package/dist/plugin/registry.d.ts +35 -0
  221. package/dist/plugin/registry.d.ts.map +1 -0
  222. package/dist/plugin/registry.js +129 -0
  223. package/dist/plugin/registry.js.map +1 -0
  224. package/dist/plugin/types.d.ts +165 -0
  225. package/dist/plugin/types.d.ts.map +1 -0
  226. package/dist/plugin/types.js +2 -0
  227. package/dist/plugin/types.js.map +1 -0
  228. package/dist/plugin/version.d.ts +34 -0
  229. package/dist/plugin/version.d.ts.map +1 -0
  230. package/dist/plugin/version.js +37 -0
  231. package/dist/plugin/version.js.map +1 -0
  232. package/dist/query-keys.d.ts +31 -0
  233. package/dist/query-keys.d.ts.map +1 -0
  234. package/dist/query-keys.js +31 -0
  235. package/dist/query-keys.js.map +1 -0
  236. package/dist/schema-editor/SchemaEditor.d.ts +43 -0
  237. package/dist/schema-editor/SchemaEditor.d.ts.map +1 -0
  238. package/dist/schema-editor/SchemaEditor.js +106 -0
  239. package/dist/schema-editor/SchemaEditor.js.map +1 -0
  240. package/dist/schema-editor/index.d.ts +10 -0
  241. package/dist/schema-editor/index.d.ts.map +1 -0
  242. package/dist/schema-editor/index.js +8 -0
  243. package/dist/schema-editor/index.js.map +1 -0
  244. package/dist/schema-editor/lint.d.ts +37 -0
  245. package/dist/schema-editor/lint.d.ts.map +1 -0
  246. package/dist/schema-editor/lint.js +49 -0
  247. package/dist/schema-editor/lint.js.map +1 -0
  248. package/dist/schema-form/SchemaForm.d.ts +70 -0
  249. package/dist/schema-form/SchemaForm.d.ts.map +1 -0
  250. package/dist/schema-form/SchemaForm.js +13 -0
  251. package/dist/schema-form/SchemaForm.js.map +1 -0
  252. package/dist/schema-form/array-field.d.ts +20 -0
  253. package/dist/schema-form/array-field.d.ts.map +1 -0
  254. package/dist/schema-form/array-field.js +39 -0
  255. package/dist/schema-form/array-field.js.map +1 -0
  256. package/dist/schema-form/classify.d.ts +87 -0
  257. package/dist/schema-form/classify.d.ts.map +1 -0
  258. package/dist/schema-form/classify.js +199 -0
  259. package/dist/schema-form/classify.js.map +1 -0
  260. package/dist/schema-form/context.d.ts +4 -0
  261. package/dist/schema-form/context.d.ts.map +1 -0
  262. package/dist/schema-form/context.js +14 -0
  263. package/dist/schema-form/context.js.map +1 -0
  264. package/dist/schema-form/default-value.d.ts +8 -0
  265. package/dist/schema-form/default-value.d.ts.map +1 -0
  266. package/dist/schema-form/default-value.js +64 -0
  267. package/dist/schema-form/default-value.js.map +1 -0
  268. package/dist/schema-form/enum-field.d.ts +19 -0
  269. package/dist/schema-form/enum-field.d.ts.map +1 -0
  270. package/dist/schema-form/enum-field.js +26 -0
  271. package/dist/schema-form/enum-field.js.map +1 -0
  272. package/dist/schema-form/field-group.d.ts +9 -0
  273. package/dist/schema-form/field-group.d.ts.map +1 -0
  274. package/dist/schema-form/field-group.js +32 -0
  275. package/dist/schema-form/field-group.js.map +1 -0
  276. package/dist/schema-form/field-node.d.ts +24 -0
  277. package/dist/schema-form/field-node.d.ts.map +1 -0
  278. package/dist/schema-form/field-node.js +51 -0
  279. package/dist/schema-form/field-node.js.map +1 -0
  280. package/dist/schema-form/index.d.ts +13 -0
  281. package/dist/schema-form/index.d.ts.map +1 -0
  282. package/dist/schema-form/index.js +11 -0
  283. package/dist/schema-form/index.js.map +1 -0
  284. package/dist/schema-form/media-field.d.ts +24 -0
  285. package/dist/schema-form/media-field.d.ts.map +1 -0
  286. package/dist/schema-form/media-field.js +169 -0
  287. package/dist/schema-form/media-field.js.map +1 -0
  288. package/dist/schema-form/media.d.ts +40 -0
  289. package/dist/schema-form/media.d.ts.map +1 -0
  290. package/dist/schema-form/media.js +65 -0
  291. package/dist/schema-form/media.js.map +1 -0
  292. package/dist/schema-form/object-fields.d.ts +20 -0
  293. package/dist/schema-form/object-fields.d.ts.map +1 -0
  294. package/dist/schema-form/object-fields.js +24 -0
  295. package/dist/schema-form/object-fields.js.map +1 -0
  296. package/dist/schema-form/resolve.d.ts +41 -0
  297. package/dist/schema-form/resolve.d.ts.map +1 -0
  298. package/dist/schema-form/resolve.js +102 -0
  299. package/dist/schema-form/resolve.js.map +1 -0
  300. package/dist/schema-form/string-field.d.ts +20 -0
  301. package/dist/schema-form/string-field.d.ts.map +1 -0
  302. package/dist/schema-form/string-field.js +68 -0
  303. package/dist/schema-form/string-field.js.map +1 -0
  304. package/dist/schema-form/styles.d.ts +18 -0
  305. package/dist/schema-form/styles.d.ts.map +1 -0
  306. package/dist/schema-form/styles.js +18 -0
  307. package/dist/schema-form/styles.js.map +1 -0
  308. package/dist/schema-form/types.d.ts +67 -0
  309. package/dist/schema-form/types.d.ts.map +1 -0
  310. package/dist/schema-form/types.js +10 -0
  311. package/dist/schema-form/types.js.map +1 -0
  312. package/dist/schema-form/union-field.d.ts +24 -0
  313. package/dist/schema-form/union-field.d.ts.map +1 -0
  314. package/dist/schema-form/union-field.js +55 -0
  315. package/dist/schema-form/union-field.js.map +1 -0
  316. package/dist/schema-form/union.d.ts +4 -0
  317. package/dist/schema-form/union.d.ts.map +1 -0
  318. package/dist/schema-form/union.js +68 -0
  319. package/dist/schema-form/union.js.map +1 -0
  320. package/dist/schema-form/unsupported-notice.d.ts +18 -0
  321. package/dist/schema-form/unsupported-notice.d.ts.map +1 -0
  322. package/dist/schema-form/unsupported-notice.js +8 -0
  323. package/dist/schema-form/unsupported-notice.js.map +1 -0
  324. package/dist/schema-form/validate.d.ts +18 -0
  325. package/dist/schema-form/validate.d.ts.map +1 -0
  326. package/dist/schema-form/validate.js +162 -0
  327. package/dist/schema-form/validate.js.map +1 -0
  328. package/dist/structured-output/StructuredOutput.d.ts +27 -0
  329. package/dist/structured-output/StructuredOutput.d.ts.map +1 -0
  330. package/dist/structured-output/StructuredOutput.js +41 -0
  331. package/dist/structured-output/StructuredOutput.js.map +1 -0
  332. package/dist/testing.d.ts +80 -0
  333. package/dist/testing.d.ts.map +1 -0
  334. package/dist/testing.js +207 -0
  335. package/dist/testing.js.map +1 -0
  336. package/package.json +101 -0
  337. package/src/components/badge.tsx +31 -0
  338. package/src/components/checkbox.tsx +93 -0
  339. package/src/components/code-block.tsx +37 -0
  340. package/src/components/completion-input.tsx +137 -0
  341. package/src/components/components.css +2212 -0
  342. package/src/components/confirm-dialog.tsx +77 -0
  343. package/src/components/control-styles.ts +14 -0
  344. package/src/components/copy-field.tsx +178 -0
  345. package/src/components/dialog.tsx +76 -0
  346. package/src/components/drawer.tsx +83 -0
  347. package/src/components/error-boundary.tsx +56 -0
  348. package/src/components/extension-combo-builder.tsx +305 -0
  349. package/src/components/extension-grouping.ts +116 -0
  350. package/src/components/extension-picker.tsx +97 -0
  351. package/src/components/external-link.tsx +40 -0
  352. package/src/components/field.tsx +169 -0
  353. package/src/components/fleet-report.tsx +136 -0
  354. package/src/components/fonts.css +22 -0
  355. package/src/components/icons.tsx +504 -0
  356. package/src/components/inputs.tsx +125 -0
  357. package/src/components/json-diff.tsx +192 -0
  358. package/src/components/json-tree.tsx +129 -0
  359. package/src/components/modal-focus.ts +150 -0
  360. package/src/components/page-header.tsx +107 -0
  361. package/src/components/primitives.tsx +489 -0
  362. package/src/components/progress-bar.tsx +100 -0
  363. package/src/components/radio-group.tsx +160 -0
  364. package/src/components/reveal-input.tsx +87 -0
  365. package/src/components/scroll-region.tsx +703 -0
  366. package/src/components/select.tsx +149 -0
  367. package/src/components/table.tsx +94 -0
  368. package/src/components/tabs.tsx +51 -0
  369. package/src/components/tags.tsx +137 -0
  370. package/src/components/tokens.css +392 -0
  371. package/src/components/tokens.ts +102 -0
  372. package/src/components/tool-picker.tsx +175 -0
  373. package/src/components/tooltip.tsx +46 -0
  374. package/src/components/version-history-panel.tsx +407 -0
  375. package/src/css.d.ts +2 -0
  376. package/src/download.ts +17 -0
  377. package/src/element-slot.ts +22 -0
  378. package/src/elicitation/ElicitationForm.tsx +87 -0
  379. package/src/errors.ts +12 -0
  380. package/src/extension-combos.ts +18 -0
  381. package/src/hooks/useApi.ts +27 -0
  382. package/src/hooks/useAuth.ts +67 -0
  383. package/src/hooks/useBreakpoint.ts +78 -0
  384. package/src/hooks/useCapabilities.ts +242 -0
  385. package/src/hooks/useSse.ts +314 -0
  386. package/src/hooks/useTheme.ts +133 -0
  387. package/src/hooks/useUnauthorized.ts +20 -0
  388. package/src/host.ts +15 -0
  389. package/src/index.ts +267 -0
  390. package/src/navigation/context.tsx +90 -0
  391. package/src/navigation/index.ts +9 -0
  392. package/src/navigation/types.ts +90 -0
  393. package/src/plugin/host-state.ts +93 -0
  394. package/src/plugin/registry.ts +174 -0
  395. package/src/plugin/types.ts +178 -0
  396. package/src/plugin/version.ts +44 -0
  397. package/src/query-keys.ts +34 -0
  398. package/src/schema-editor/SchemaEditor.tsx +186 -0
  399. package/src/schema-editor/index.ts +9 -0
  400. package/src/schema-editor/lint.ts +70 -0
  401. package/src/schema-form/SchemaForm.tsx +107 -0
  402. package/src/schema-form/array-field.tsx +110 -0
  403. package/src/schema-form/classify.ts +302 -0
  404. package/src/schema-form/context.ts +17 -0
  405. package/src/schema-form/default-value.ts +65 -0
  406. package/src/schema-form/enum-field.tsx +65 -0
  407. package/src/schema-form/field-group.tsx +71 -0
  408. package/src/schema-form/field-node.tsx +161 -0
  409. package/src/schema-form/index.ts +12 -0
  410. package/src/schema-form/media-field.tsx +281 -0
  411. package/src/schema-form/media.ts +69 -0
  412. package/src/schema-form/object-fields.tsx +74 -0
  413. package/src/schema-form/resolve.ts +114 -0
  414. package/src/schema-form/string-field.tsx +161 -0
  415. package/src/schema-form/styles.ts +20 -0
  416. package/src/schema-form/types.ts +71 -0
  417. package/src/schema-form/union-field.tsx +122 -0
  418. package/src/schema-form/union.ts +78 -0
  419. package/src/schema-form/unsupported-notice.tsx +39 -0
  420. package/src/schema-form/validate.ts +183 -0
  421. package/src/structured-output/StructuredOutput.tsx +81 -0
  422. package/src/testing.ts +235 -0
@@ -0,0 +1,174 @@
1
+ /**
2
+ * The in-memory registry the shell reads, plus {@link loadPlugin} — the one entry
3
+ * through which a plugin bundle's `register(context)` contributes to it. This module
4
+ * is bundled only into the served `@tai42/studio-sdk/host` singleton (external across
5
+ * the plugin boundary, like react and react-dom), so the module-level registry is
6
+ * the SAME instance every host caller — the shell and the feature panels — sees. The
7
+ * plugin surface (`@tai42/studio-sdk`) does not include it, so a plugin cannot reach it.
8
+ */
9
+ import type {
10
+ NavEntryContribution,
11
+ PageContribution,
12
+ PluginContext,
13
+ PluginContributions,
14
+ PluginEntry,
15
+ RegisteredNavEntry,
16
+ RegisteredPage,
17
+ RegisteredSettingsTab,
18
+ SettingsTabContribution,
19
+ ToolPanelContribution,
20
+ } from './types';
21
+
22
+ const toolPanels = new Map<string, ToolPanelContribution>();
23
+ const pages: RegisteredPage[] = [];
24
+ const settingsTabs: RegisteredSettingsTab[] = [];
25
+ const navEntries: RegisteredNavEntry[] = [];
26
+
27
+ /**
28
+ * Load one plugin: call its `register` entry with a context bound to `pluginId`,
29
+ * then commit everything it staged into the global registry.
30
+ *
31
+ * Contributions are STAGED into local arrays (identity captured in the closure,
32
+ * never read from ambient state) and only committed after `entry` settles
33
+ * successfully. `entry` is AWAITED before commit, so a synchronous or an `async`
34
+ * entry is fully done first: a post-`await` throw skips the commit (atomicity
35
+ * holds for async too) and a post-`await` registration cannot slip past the batch.
36
+ * The seal is set after `entry` has SETTLED — for a sync entry, after it returns
37
+ * and the `await` continuation runs; for an async entry, after its returned promise
38
+ * resolves. A registration attempted from that point on (a deferred timer, a
39
+ * callback scheduled after settle) throws loudly instead of pushing into an
40
+ * orphaned staged array that never commits. Registrations made during `entry`'s
41
+ * synchronous body — including a microtask it enqueues then, which runs before the
42
+ * `await` continuation — are still captured and committed with the batch. A
43
+ * `register` that throws commits nothing and leaves the registry untouched.
44
+ * Duplicate guards run against BOTH the staged contributions and what is already
45
+ * committed, and fail loudly.
46
+ */
47
+ export async function loadPlugin(pluginId: string, entry: PluginEntry): Promise<void> {
48
+ const stagedPages: RegisteredPage[] = [];
49
+ const stagedToolPanels = new Map<string, ToolPanelContribution>();
50
+ const stagedSettingsTabs: RegisteredSettingsTab[] = [];
51
+ const stagedTabIds = new Set<string>();
52
+ const stagedNavEntries: RegisteredNavEntry[] = [];
53
+ let sealed = false;
54
+
55
+ const context: PluginContext = {
56
+ registerPage(contribution: PageContribution): void {
57
+ if (sealed) {
58
+ throw new Error(
59
+ 'registration is closed: a plugin must register during register(), not after it resolves',
60
+ );
61
+ }
62
+ const staged = stagedPages.some((p) => p.path === contribution.path);
63
+ const committed = pages.some((p) => p.pluginId === pluginId && p.path === contribution.path);
64
+ if (staged || committed) {
65
+ // A duplicate path within one plugin is an author mistake that would
66
+ // leave one page unreachable — fail loudly, as registerToolPanel does.
67
+ throw new Error(`plugin “${pluginId}” already registered a page at “${contribution.path}”`);
68
+ }
69
+ stagedPages.push({ ...contribution, pluginId });
70
+ },
71
+ registerToolPanel(contribution: ToolPanelContribution): void {
72
+ if (sealed) {
73
+ throw new Error(
74
+ 'registration is closed: a plugin must register during register(), not after it resolves',
75
+ );
76
+ }
77
+ if (stagedToolPanels.has(contribution.toolName) || toolPanels.has(contribution.toolName)) {
78
+ throw new Error(`a tool panel is already registered for ${contribution.toolName}`);
79
+ }
80
+ stagedToolPanels.set(contribution.toolName, contribution);
81
+ },
82
+ registerSettingsTab(contribution: SettingsTabContribution): void {
83
+ if (sealed) {
84
+ throw new Error(
85
+ 'registration is closed: a plugin must register during register(), not after it resolves',
86
+ );
87
+ }
88
+ const committed = settingsTabs.some(
89
+ (t) => t.pluginId === pluginId && t.id === contribution.id,
90
+ );
91
+ if (stagedTabIds.has(contribution.id) || committed) {
92
+ // A duplicate tab id within one plugin would leave one tab unreachable —
93
+ // fail loudly, mirroring the page-path guard.
94
+ throw new Error(
95
+ `plugin “${pluginId}” already registered a settings tab “${contribution.id}”`,
96
+ );
97
+ }
98
+ stagedTabIds.add(contribution.id);
99
+ stagedSettingsTabs.push({ ...contribution, pluginId });
100
+ },
101
+ registerNavEntry(contribution: NavEntryContribution): void {
102
+ if (sealed) {
103
+ throw new Error(
104
+ 'registration is closed: a plugin must register during register(), not after it resolves',
105
+ );
106
+ }
107
+ const staged = stagedNavEntries.some((e) => e.path === contribution.path);
108
+ const committed = navEntries.some(
109
+ (e) => e.pluginId === pluginId && e.path === contribution.path,
110
+ );
111
+ if (staged || committed) {
112
+ // A duplicate nav path within one plugin is an author mistake — two
113
+ // entries pointing at the same page — so fail loudly like the page guard.
114
+ throw new Error(
115
+ `plugin “${pluginId}” already registered a nav entry at “${contribution.path}”`,
116
+ );
117
+ }
118
+ stagedNavEntries.push({ ...contribution, pluginId });
119
+ },
120
+ };
121
+
122
+ try {
123
+ await entry(context);
124
+ } finally {
125
+ // Whether `entry` resolved or threw, no further registration is valid — seal
126
+ // so a deferred call fails loudly rather than pushing into an orphaned array.
127
+ sealed = true;
128
+ }
129
+
130
+ // Every nav entry must link to a page THIS plugin registers — staged in this
131
+ // call or already committed. A nav entry with no matching page is a guaranteed
132
+ // dead link (the plugin page surface renders "Page not found"), so reject it
133
+ // loudly. This throws BEFORE the commit loops, so it propagates out of
134
+ // `loadPlugin` and NOTHING (pages, panels, tabs, nav) commits — atomicity holds.
135
+ for (const navEntry of stagedNavEntries) {
136
+ const hasPage =
137
+ stagedPages.some((p) => p.path === navEntry.path) ||
138
+ pages.some((p) => p.pluginId === pluginId && p.path === navEntry.path);
139
+ if (!hasPage) {
140
+ throw new Error(
141
+ `plugin “${pluginId}” registered a nav entry at “${navEntry.path}” but no page at that path`,
142
+ );
143
+ }
144
+ }
145
+
146
+ // `entry` resolved without throwing: commit the staged contributions as one
147
+ // batch, so the registry only ever holds a plugin's complete set — never a
148
+ // partial set left behind by a mid-registration failure.
149
+ for (const page of stagedPages) {
150
+ pages.push(page);
151
+ }
152
+ for (const [toolName, contribution] of stagedToolPanels) {
153
+ toolPanels.set(toolName, contribution);
154
+ }
155
+ for (const tab of stagedSettingsTabs) {
156
+ settingsTabs.push(tab);
157
+ }
158
+ for (const navEntry of stagedNavEntries) {
159
+ navEntries.push(navEntry);
160
+ }
161
+ }
162
+
163
+ /** The shell reads this after loading every installed plugin bundle. */
164
+ export function getContributions(): PluginContributions {
165
+ return { toolPanels, pages, settingsTabs, navEntries };
166
+ }
167
+
168
+ /** Tests reset the module-level registry between cases. */
169
+ export function __resetContributions(): void {
170
+ toolPanels.clear();
171
+ pages.length = 0;
172
+ settingsTabs.length = 0;
173
+ navEntries.length = 0;
174
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * The Studio-plugin authoring contract. A platform plugin ships an ESM bundle
3
+ * that exports a `register(context)` entry ({@link PluginEntry}); the host calls
4
+ * it with a {@link PluginContext} through which the plugin contributes tool
5
+ * panels, pages, settings tabs, and sidebar nav entries.
6
+ *
7
+ * Two styling paths are sanctioned, and only these two:
8
+ * 1. SDK components plus inline styles that read the design-system tokens
9
+ * (`var(--tai-*)`). Raw Tailwind utilities are host-internal, not part of
10
+ * this contract.
11
+ * 2. A plugin-shipped SCOPED stylesheet, listed in the bundle's manifest
12
+ * `integrity` map as a `.css` asset and HOST-injected as an SRI'd
13
+ * `<link rel="stylesheet">` before the bundle's JS is imported. It is bound
14
+ * by three hard rules: (a) no global resets/preflight — never style `html`,
15
+ * `body`, `:root`, `*`, or bare element selectors at top level; (b) every
16
+ * selector is scoped under a plugin root class the plugin renders itself,
17
+ * prefixed with the plugin's own package name; (c) theme values in
18
+ * PLUGIN-AUTHORED rules come from the SDK custom properties (`var(--tai-*)`),
19
+ * never hardcoded colors, so the stylesheet themes itself with no logic of
20
+ * its own. Bundled third-party base CSS is exempt from rule (c) but rules
21
+ * (a) and (b) still bind it.
22
+ */
23
+ import type { ComponentType } from 'react';
24
+
25
+ /**
26
+ * A capability requirement a contribution may declare. `routes` is a list of
27
+ * route-path prefixes (anyOf semantics, the same evaluator the shell nav uses):
28
+ * the contribution renders iff the caller's capability projection covers at least
29
+ * one of them. ABSENT (the field is optional) means the contribution renders only
30
+ * for a FULL projection — safe-by-default for every existing plugin, which never
31
+ * declared a requirement. The server remains the authority; this only shapes what
32
+ * the UI advertises.
33
+ */
34
+ export interface RequiredCapabilities {
35
+ readonly routes: readonly string[];
36
+ }
37
+
38
+ /** Props a contributed tool-run panel receives (replaces the auto-form). */
39
+ export interface ToolPanelProps {
40
+ /** The tool name this panel targets. */
41
+ readonly toolName: string;
42
+ /** The tool's JSON schema (Pydantic-emitted), for panels that introspect it. */
43
+ readonly schema: Record<string, unknown>;
44
+ /** Run the tool with arguments; resolves with the typed result or throws. */
45
+ readonly run: (args: Record<string, unknown>) => Promise<unknown>;
46
+ }
47
+
48
+ /** Props a contributed full page receives. Navigation is via shell route tokens. */
49
+ export interface PluginPageProps {
50
+ readonly pluginId: string;
51
+ }
52
+
53
+ /** Props a contributed settings tab receives. */
54
+ export interface SettingsTabProps {
55
+ readonly pluginId: string;
56
+ }
57
+
58
+ export interface ToolPanelContribution {
59
+ readonly toolName: string;
60
+ readonly component: ComponentType<ToolPanelProps>;
61
+ }
62
+
63
+ export interface PageContribution {
64
+ /** URL segment under `/plugins/{pluginId}/`. */
65
+ readonly path: string;
66
+ readonly title: string;
67
+ readonly component: ComponentType<PluginPageProps>;
68
+ /** Capability gate (see {@link RequiredCapabilities}); absent ⇒ full-only. */
69
+ readonly requiredCapabilities?: RequiredCapabilities;
70
+ }
71
+
72
+ /**
73
+ * A page as stored in the registry: the plugin's {@link PageContribution} plus
74
+ * the id of the plugin that registered it. The `pluginId` is stamped by the
75
+ * registry from the identity the host passed to {@link PluginEntry} — a plugin
76
+ * never supplies its own id — so a page resolves only under its owner's
77
+ * `/plugins/{pluginId}/` prefix and two plugins may register the same `path`
78
+ * without colliding.
79
+ */
80
+ export interface RegisteredPage extends PageContribution {
81
+ readonly pluginId: string;
82
+ }
83
+
84
+ export interface NavEntryContribution {
85
+ /**
86
+ * Path of a page THIS plugin registers. The nav entry links to
87
+ * `/plugins/{pluginId}/{path}`, so it must match a {@link PageContribution}
88
+ * `path` of the same plugin — the registry rejects a nav entry with no page.
89
+ */
90
+ readonly path: string;
91
+ readonly title: string;
92
+ /**
93
+ * Optional icon rendered before the title. It must be a square inline SVG that
94
+ * fills its box and draws with `currentColor`; the host constrains the slot,
95
+ * rendering it inside a fixed 1em box, `aria-hidden` (the accessible name is
96
+ * `title`), with the color inherited from the link. Its bytes live inside the
97
+ * plugin bundle, so no external fetch and no CSP change. Absent icon renders a
98
+ * text-only entry, exactly like the core nav.
99
+ */
100
+ readonly icon?: ComponentType;
101
+ /** Capability gate (see {@link RequiredCapabilities}); absent ⇒ full-only. */
102
+ readonly requiredCapabilities?: RequiredCapabilities;
103
+ }
104
+
105
+ /**
106
+ * A nav entry as stored in the registry: the plugin's
107
+ * {@link NavEntryContribution} plus the id of the plugin that registered it. The
108
+ * `pluginId` is stamped by the registry from the identity the host passed to
109
+ * {@link PluginEntry}, so the entry links only under its owner's
110
+ * `/plugins/{pluginId}/` prefix and two plugins may register the same `path`
111
+ * without colliding.
112
+ */
113
+ export interface RegisteredNavEntry extends NavEntryContribution {
114
+ readonly pluginId: string;
115
+ }
116
+
117
+ export interface SettingsTabContribution {
118
+ readonly id: string;
119
+ readonly title: string;
120
+ readonly component: ComponentType<SettingsTabProps>;
121
+ /** Capability gate (see {@link RequiredCapabilities}); absent ⇒ full-only. */
122
+ readonly requiredCapabilities?: RequiredCapabilities;
123
+ }
124
+
125
+ /**
126
+ * A settings tab as stored in the registry: the plugin's
127
+ * {@link SettingsTabContribution} plus the id of the plugin that registered it.
128
+ * The `pluginId` is stamped by the registry from the identity the host passed to
129
+ * {@link PluginEntry} — a plugin never supplies its own id — so a tab is always
130
+ * attributable to its owner and two plugins may register the same tab `id`
131
+ * without colliding.
132
+ */
133
+ export interface RegisteredSettingsTab extends SettingsTabContribution {
134
+ readonly pluginId: string;
135
+ }
136
+
137
+ /** Everything a single plugin bundle has registered at import time. */
138
+ export interface PluginContributions {
139
+ readonly toolPanels: ReadonlyMap<string, ToolPanelContribution>;
140
+ readonly pages: readonly RegisteredPage[];
141
+ readonly settingsTabs: readonly RegisteredSettingsTab[];
142
+ readonly navEntries: readonly RegisteredNavEntry[];
143
+ }
144
+
145
+ /**
146
+ * The registration surface a plugin receives in its `register(context)` entry.
147
+ * Each method binds the contribution to the plugin the host is loading — the
148
+ * plugin's identity is fixed by the host, not read from any ambient state — so a
149
+ * contribution can never be misattributed to another plugin. Contributions are
150
+ * staged and committed together once `register` settles; a `register` that throws
151
+ * commits nothing. The context is SEALED once `register` settles: a call made
152
+ * after that (a deferred timer, a post-resolve microtask) throws instead of
153
+ * silently dropping, so every registration must happen during `register`.
154
+ */
155
+ export interface PluginContext {
156
+ /** Register a full page mounted under `/plugins/{pluginId}/{path}`. */
157
+ registerPage(contribution: PageContribution): void;
158
+ /** Register a rich run panel for a tool by name (overrides the auto-form). */
159
+ registerToolPanel(contribution: ToolPanelContribution): void;
160
+ /** Register a settings tab. */
161
+ registerSettingsTab(contribution: SettingsTabContribution): void;
162
+ /**
163
+ * Register a sidebar nav entry linking to one of this plugin's pages at
164
+ * `/plugins/{pluginId}/{path}`. The `path` must match a page the plugin also
165
+ * registers (a nav entry with no page is a dead link and is rejected loudly).
166
+ */
167
+ registerNavEntry(contribution: NavEntryContribution): void;
168
+ }
169
+
170
+ /**
171
+ * A plugin bundle's entry: the default plugin API. The host imports the bundle,
172
+ * reads its `register` export, and calls it with a {@link PluginContext}. All
173
+ * contributions flow through that context — there are no free registration
174
+ * functions. The entry may be synchronous or `async`; the host awaits it before
175
+ * committing, so an async entry must complete every registration before it
176
+ * resolves (see the seal on {@link PluginContext}).
177
+ */
178
+ export type PluginEntry = (context: PluginContext) => void | Promise<void>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The Studio-plugin API compatibility version.
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).
7
+ *
8
+ * Gate rule: equality is correct precisely because additive changes never
9
+ * bump (existing plugins keep matching) and after a break every plugin must
10
+ * rebuild by design. `targeted == current` accepts; anything else rejects.
11
+ *
12
+ * Additive evolution under this gate: a host that gains a NEW optional context
13
+ * capability (a new `register*` method, a new host-side injection behavior)
14
+ * keeps the same version, so every existing plugin loads unchanged. The one
15
+ * asymmetry the equality gate leaves is a plugin that USES a newer capability on
16
+ * an OLDER host that lacks it — and it fails loudly, by construction, not by any
17
+ * negotiation machinery:
18
+ * - A plugin calling a context method the host does not define throws the
19
+ * natural `context.<method> is not a function` during `register`; the loader
20
+ * catches it as that plugin's loud error card and commits nothing.
21
+ * - A plugin shipping a `.css` asset to a host that does not inject stylesheets
22
+ * is served but never injected — the page renders with SDK-component styling
23
+ * only. This is the single quiet degradation the mechanism admits, and the
24
+ * authoring docs state it: stylesheet injection is a host capability, so a
25
+ * deployment pairs the Studio host with plugins built for it.
26
+ */
27
+ export const STUDIO_PLUGIN_API_VERSION = 1;
28
+
29
+ export interface VersionGateResult {
30
+ readonly ok: boolean;
31
+ readonly reason?: string;
32
+ }
33
+
34
+ /** Accept iff the plugin's targeted version exactly equals the host's. */
35
+ export function checkPluginApiVersion(
36
+ targeted: number,
37
+ current = STUDIO_PLUGIN_API_VERSION,
38
+ ): VersionGateResult {
39
+ if (targeted === current) return { ok: true };
40
+ return {
41
+ ok: false,
42
+ reason: `plugin targets Studio API v${String(targeted)} but this host is v${String(current)} — the plugin must be rebuilt`,
43
+ };
44
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Cross-feature TanStack Query keys.
3
+ *
4
+ * Feature packages cannot import each other — the SDK is their one shared seam — so
5
+ * a query key more than one feature must reference lives here as a single
6
+ * authoritative constant rather than a duplicated literal that could drift.
7
+ *
8
+ * `toolsListKey` is the registered-tool master list. A mutation that changes the
9
+ * registered-tool population — creating, deleting, or rolling back a preset (each
10
+ * binds or tears down a live tool) — invalidates it so the tools page refetches.
11
+ */
12
+
13
+ /** The registered-tool master list (`GET /api/tools`). */
14
+ export const toolsListKey = ['tools'] as const;
15
+
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.
21
+ */
22
+ export const extensionsQueryKey = ['extensions'] as const;
23
+
24
+ /**
25
+ * The discovered sub-MCP mounts (`GET /api/sub-mcp`). The settings feature reads
26
+ * this map (its scope mapper lists the mounts) while the manifest feature owns the
27
+ * create/delete controls that mutate it; both must key and invalidate the exact
28
+ * same tuple, so it lives here rather than as a literal duplicated across the two
29
+ * features (which cannot import each other).
30
+ */
31
+ export const subMcpKey = ['sub-mcp'] as const;
32
+
33
+ /** The api-key payloads; read by settings AND the hooks execution-key picker. */
34
+ export const tokensPayloadKey = ['auth-tokens-payload'] as const;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * `SchemaEditor` — the shared control for AUTHORING a JSON Schema. It is a VALIDATED
3
+ * code editor, NOT a visual builder: the operator writes the schema as JSON in a
4
+ * textarea, and the editor gives loud parse feedback, a structural lint (top-level
5
+ * object; a `"title"` when `requireTitle`), and a live PREVIEW of the authored shape
6
+ * through the same {@link SchemaForm} renderer the run side uses (falling back to a
7
+ * {@link JsonTree} of the schema dict when the shape is not form-renderable).
8
+ *
9
+ * A JSON Schema is more expressive than any builder UI (`$defs`, `anyOf`, constraint
10
+ * keywords), and the backend rejects an invalid schema LOUDLY — so full meta-schema
11
+ * validation stays server-side and this control adds no schema-validation dependency.
12
+ *
13
+ * CONTROLLED-BY-SEED: `value` seeds the editor's text ONCE (on mount); thereafter the
14
+ * text is the source of truth and every edit reports up through `onChange`. Validity
15
+ * is exposed there too so the consumer can gate its submit — always alongside a
16
+ * VISIBLE inline message, never a silently disabled button. The parsed dict is passed
17
+ * through untouched, so a schema the editor does not understand round-trips exactly.
18
+ *
19
+ * SAFETY: the schema text is operator-authored and every derived value renders as
20
+ * React TEXT (through the DS controls / `JsonTree` / `SchemaForm`), never an HTML sink.
21
+ */
22
+ import { Component, useMemo, useState, type ReactNode } from 'react';
23
+
24
+ import { Field } from '../components/field';
25
+ import { Textarea } from '../components/inputs';
26
+ import { JsonTree } from '../components/json-tree';
27
+ import { ScrollRegion } from '../components/scroll-region';
28
+ import { classifySchema } from '../schema-form/classify';
29
+ import { defaultValueForSchema } from '../schema-form/default-value';
30
+ import { SchemaForm } from '../schema-form/SchemaForm';
31
+ import type { JsonSchema } from '../schema-form/types';
32
+ import { lintSchemaText } from './lint';
33
+
34
+ /** What {@link SchemaEditor} reports up on every edit. */
35
+ export interface SchemaEditorChange {
36
+ /** The authored schema dict when valid and non-empty; `null` when the editor is empty. */
37
+ readonly schema: Record<string, unknown> | null;
38
+ /** False when the text is non-empty but unparseable or failing lint — gate submit on this. */
39
+ readonly valid: boolean;
40
+ }
41
+
42
+ export interface SchemaEditorProps {
43
+ /** The schema dict to seed the editor with, or `null` for an empty editor. */
44
+ readonly value: Record<string, unknown> | null;
45
+ /** Fired on every edit with the parsed schema (or `null`) and its validity. */
46
+ readonly onChange: (change: SchemaEditorChange) => void;
47
+ /** When true, a top-level `"title"` is required (the `response_format` contract). */
48
+ readonly requireTitle: boolean;
49
+ readonly label?: string;
50
+ readonly description?: string;
51
+ readonly disabled?: boolean;
52
+ readonly idPrefix?: string;
53
+ }
54
+
55
+ function seedText(value: Record<string, unknown> | null): string {
56
+ return value === null ? '' : JSON.stringify(value, null, 2);
57
+ }
58
+
59
+ /**
60
+ * Whether {@link SchemaForm} can render this schema's root — i.e. it classifies to a
61
+ * concrete field kind rather than `unsupported`. A schema whose root the form cannot
62
+ * render (or whose classification throws on a bad `$ref`) previews as a `JsonTree`.
63
+ */
64
+ function canRenderWithForm(schema: JsonSchema): boolean {
65
+ try {
66
+ return classifySchema(schema, schema).model.kind !== 'unsupported';
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * An interactive preview of the authored schema. Seeds a throwaway value from the
74
+ * schema's defaults so the operator can see (and poke) the shape they defined.
75
+ */
76
+ function PreviewForm({ schema }: { readonly schema: JsonSchema }): ReactNode {
77
+ const [value, setValue] = useState<unknown>(() => defaultValueForSchema(schema));
78
+ return <SchemaForm schema={schema} value={value} onChange={setValue} idPrefix="schema-preview" />;
79
+ }
80
+
81
+ /**
82
+ * Catch a render-time throw from a deep, malformed schema (e.g. a nested unresolvable
83
+ * `$ref`) so the preview degrades to the `JsonTree` fallback instead of crashing the
84
+ * enclosing dialog. Reset per authored schema by keying this boundary on the schema.
85
+ */
86
+ class PreviewBoundary extends Component<
87
+ { readonly fallback: ReactNode; readonly children: ReactNode },
88
+ { readonly failed: boolean }
89
+ > {
90
+ override state = { failed: false };
91
+ static getDerivedStateFromError(_error: unknown): { failed: boolean } {
92
+ return { failed: true };
93
+ }
94
+ override render(): ReactNode {
95
+ return this.state.failed ? this.props.fallback : this.props.children;
96
+ }
97
+ }
98
+
99
+ /** The preview pane's accessible name, wherever the scrolling box turns out to be. */
100
+ const PREVIEW_LABEL = 'Schema preview';
101
+
102
+ /**
103
+ * The preview pane, framed as a card and scrollable — a deep schema's preview can
104
+ * outrun its column, so it scrolls in place rather than pushing the dialog sideways.
105
+ *
106
+ * WHICH element scrolls depends on the branch, and only the one that actually
107
+ * scrolls may carry the region attributes. {@link JsonTree} IS its own scrolling
108
+ * box, so on the fallback branch the card is a plain frame and the tree names
109
+ * itself; a `ScrollRegion` around it would take an overflow that never reaches it,
110
+ * leaving a name announced for a box that cannot move. The {@link SchemaForm}
111
+ * branch renders no scroller of its own, so there the card IS the region.
112
+ */
113
+ function SchemaPreview({
114
+ schema,
115
+ testId,
116
+ }: {
117
+ readonly schema: Record<string, unknown>;
118
+ readonly testId: string;
119
+ }): ReactNode {
120
+ const jsonFallback = (
121
+ <div className="tai-card" data-testid={testId}>
122
+ <JsonTree data={schema} label={PREVIEW_LABEL} />
123
+ </div>
124
+ );
125
+ if (!canRenderWithForm(schema)) return jsonFallback;
126
+ return (
127
+ <PreviewBoundary fallback={jsonFallback}>
128
+ <ScrollRegion label={PREVIEW_LABEL} className="tai-card" data-testid={testId}>
129
+ <PreviewForm schema={schema} />
130
+ </ScrollRegion>
131
+ </PreviewBoundary>
132
+ );
133
+ }
134
+
135
+ export function SchemaEditor({
136
+ value,
137
+ onChange,
138
+ requireTitle,
139
+ label = 'Schema',
140
+ description,
141
+ disabled,
142
+ idPrefix = 'schema-editor',
143
+ }: SchemaEditorProps): ReactNode {
144
+ const [text, setText] = useState<string>(() => seedText(value));
145
+
146
+ // Recomputed for display (inline error + preview) on every render; the change
147
+ // handler lints the NEXT text independently so `onChange` and the view agree.
148
+ const result = useMemo(() => lintSchemaText(text, requireTitle), [text, requireTitle]);
149
+
150
+ const emit = (nextText: string): void => {
151
+ setText(nextText);
152
+ const next = lintSchemaText(nextText, requireTitle);
153
+ onChange({ schema: next.valid ? next.schema : null, valid: next.valid });
154
+ };
155
+
156
+ return (
157
+ <div data-testid={idPrefix} className="tai-stack tai-stack-3">
158
+ <Field label={label} description={description} error={result.error}>
159
+ <Textarea
160
+ value={text}
161
+ disabled={disabled}
162
+ onChange={(event) => {
163
+ emit(event.target.value);
164
+ }}
165
+ rows={10}
166
+ aria-label={`${label} JSON`}
167
+ className="tai-textarea-mono"
168
+ spellCheck={false}
169
+ />
170
+ </Field>
171
+
172
+ {result.schema !== null ? (
173
+ <div className="tai-stack tai-stack-2">
174
+ <span className="tai-label">Preview</span>
175
+ <SchemaPreview
176
+ // Re-seed the preview (and reset its error boundary) whenever the
177
+ // authored schema changes.
178
+ key={JSON.stringify(result.schema)}
179
+ schema={result.schema}
180
+ testId={`${idPrefix}-preview`}
181
+ />
182
+ </div>
183
+ ) : null}
184
+ </div>
185
+ );
186
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Schema-editor — the shared, validated JSON-Schema authoring control. Public
3
+ * surface: the `SchemaEditor` component, its change type, and the pure lint helper
4
+ * consumers reuse to pre-check a stored schema.
5
+ */
6
+ export { SchemaEditor } from './SchemaEditor';
7
+ export type { SchemaEditorProps, SchemaEditorChange } from './SchemaEditor';
8
+ export { lintSchemaText } from './lint';
9
+ export type { SchemaLintResult } from './lint';