@tai42/studio-sdk 15.0.0 → 16.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/dist/components/explorer-body.d.ts +49 -0
  2. package/dist/components/explorer-body.d.ts.map +1 -0
  3. package/dist/components/explorer-body.js +67 -0
  4. package/dist/components/explorer-body.js.map +1 -0
  5. package/dist/components/explorer-page-size.d.ts +4 -0
  6. package/dist/components/explorer-page-size.d.ts.map +1 -0
  7. package/dist/components/explorer-page-size.js +36 -0
  8. package/dist/components/explorer-page-size.js.map +1 -0
  9. package/dist/components/explorer-tag-filter.d.ts +17 -0
  10. package/dist/components/explorer-tag-filter.d.ts.map +1 -0
  11. package/dist/components/explorer-tag-filter.js +29 -0
  12. package/dist/components/explorer-tag-filter.js.map +1 -0
  13. package/dist/components/explorer-tags.d.ts +22 -0
  14. package/dist/components/explorer-tags.d.ts.map +1 -0
  15. package/dist/components/explorer-tags.js +51 -0
  16. package/dist/components/explorer-tags.js.map +1 -0
  17. package/dist/components/explorer-view.d.ts +3 -17
  18. package/dist/components/explorer-view.d.ts.map +1 -1
  19. package/dist/components/explorer-view.js +53 -170
  20. package/dist/components/explorer-view.js.map +1 -1
  21. package/dist/components/extension-combo-builder.d.ts +0 -5
  22. package/dist/components/extension-combo-builder.d.ts.map +1 -1
  23. package/dist/components/extension-combo-builder.js +4 -42
  24. package/dist/components/extension-combo-builder.js.map +1 -1
  25. package/dist/components/extension-combo-parts.d.ts +44 -0
  26. package/dist/components/extension-combo-parts.d.ts.map +1 -0
  27. package/dist/components/extension-combo-parts.js +57 -0
  28. package/dist/components/extension-combo-parts.js.map +1 -0
  29. package/dist/components/fleet-report.d.ts.map +1 -1
  30. package/dist/components/fleet-report.js +51 -37
  31. package/dist/components/fleet-report.js.map +1 -1
  32. package/dist/components/icons/chrome-marks.d.ts +50 -0
  33. package/dist/components/icons/chrome-marks.d.ts.map +1 -0
  34. package/dist/components/icons/chrome-marks.js +51 -0
  35. package/dist/components/icons/chrome-marks.js.map +1 -0
  36. package/dist/components/icons/icon-frame.d.ts +29 -0
  37. package/dist/components/icons/icon-frame.d.ts.map +1 -0
  38. package/dist/components/icons/icon-frame.js +19 -0
  39. package/dist/components/icons/icon-frame.js.map +1 -0
  40. package/dist/components/icons/index.d.ts +40 -0
  41. package/dist/components/icons/index.d.ts.map +1 -0
  42. package/dist/components/icons/index.js +6 -0
  43. package/dist/components/icons/index.js.map +1 -0
  44. package/dist/components/icons/nav-map.d.ts +14 -0
  45. package/dist/components/icons/nav-map.d.ts.map +1 -0
  46. package/dist/components/icons/nav-map.js +28 -0
  47. package/dist/components/icons/nav-map.js.map +1 -0
  48. package/dist/components/icons/nav-marks.d.ts +44 -0
  49. package/dist/components/icons/nav-marks.d.ts.map +1 -0
  50. package/dist/components/icons/nav-marks.js +45 -0
  51. package/dist/components/icons/nav-marks.js.map +1 -0
  52. package/dist/components/icons/status-marks.d.ts +21 -0
  53. package/dist/components/icons/status-marks.d.ts.map +1 -0
  54. package/dist/components/icons/status-marks.js +22 -0
  55. package/dist/components/icons/status-marks.js.map +1 -0
  56. package/dist/components/icons/theme-marks.d.ts +9 -0
  57. package/dist/components/icons/theme-marks.d.ts.map +1 -0
  58. package/dist/components/icons/theme-marks.js +10 -0
  59. package/dist/components/icons/theme-marks.js.map +1 -0
  60. package/dist/components/json-diff.d.ts.map +1 -1
  61. package/dist/components/json-diff.js +1 -3
  62. package/dist/components/json-diff.js.map +1 -1
  63. package/dist/components/json-tree-context.d.ts +13 -0
  64. package/dist/components/json-tree-context.d.ts.map +1 -0
  65. package/dist/components/json-tree-context.js +15 -0
  66. package/dist/components/json-tree-context.js.map +1 -0
  67. package/dist/components/json-tree-model.d.ts +56 -0
  68. package/dist/components/json-tree-model.d.ts.map +1 -0
  69. package/dist/components/json-tree-model.js +125 -0
  70. package/dist/components/json-tree-model.js.map +1 -0
  71. package/dist/components/json-tree-nodes.d.ts +30 -0
  72. package/dist/components/json-tree-nodes.d.ts.map +1 -0
  73. package/dist/components/json-tree-nodes.js +118 -0
  74. package/dist/components/json-tree-nodes.js.map +1 -0
  75. package/dist/components/json-tree.d.ts.map +1 -1
  76. package/dist/components/json-tree.js +7 -295
  77. package/dist/components/json-tree.js.map +1 -1
  78. package/dist/components/json-value-format.d.ts +20 -0
  79. package/dist/components/json-value-format.d.ts.map +1 -0
  80. package/dist/components/json-value-format.js +63 -0
  81. package/dist/components/json-value-format.js.map +1 -0
  82. package/dist/components/markdown-blocks.d.ts +38 -0
  83. package/dist/components/markdown-blocks.d.ts.map +1 -0
  84. package/dist/components/markdown-blocks.js +168 -0
  85. package/dist/components/markdown-blocks.js.map +1 -0
  86. package/dist/components/markdown-inline.d.ts +24 -0
  87. package/dist/components/markdown-inline.d.ts.map +1 -0
  88. package/dist/components/markdown-inline.js +257 -0
  89. package/dist/components/markdown-inline.js.map +1 -0
  90. package/dist/components/markdown.d.ts +3 -52
  91. package/dist/components/markdown.d.ts.map +1 -1
  92. package/dist/components/markdown.js +4 -376
  93. package/dist/components/markdown.js.map +1 -1
  94. package/dist/components/observer-registry.d.ts +38 -0
  95. package/dist/components/observer-registry.d.ts.map +1 -0
  96. package/dist/components/observer-registry.js +131 -0
  97. package/dist/components/observer-registry.js.map +1 -0
  98. package/dist/components/overflow-measure.d.ts +38 -0
  99. package/dist/components/overflow-measure.d.ts.map +1 -0
  100. package/dist/components/overflow-measure.js +54 -0
  101. package/dist/components/overflow-measure.js.map +1 -0
  102. package/dist/components/prose-regions.d.ts +81 -0
  103. package/dist/components/prose-regions.d.ts.map +1 -0
  104. package/dist/components/prose-regions.js +128 -0
  105. package/dist/components/prose-regions.js.map +1 -0
  106. package/dist/components/scroll-region.d.ts.map +1 -1
  107. package/dist/components/scroll-region.js +3 -298
  108. package/dist/components/scroll-region.js.map +1 -1
  109. package/dist/components/tab-stop-parse.d.ts +53 -0
  110. package/dist/components/tab-stop-parse.d.ts.map +1 -0
  111. package/dist/components/tab-stop-parse.js +202 -0
  112. package/dist/components/tab-stop-parse.js.map +1 -0
  113. package/dist/components/templated-text-field.d.ts.map +1 -1
  114. package/dist/components/templated-text-field.js +10 -148
  115. package/dist/components/templated-text-field.js.map +1 -1
  116. package/dist/components/templated-text-kwargs-editor.d.ts +24 -0
  117. package/dist/components/templated-text-kwargs-editor.d.ts.map +1 -0
  118. package/dist/components/templated-text-kwargs-editor.js +48 -0
  119. package/dist/components/templated-text-kwargs-editor.js.map +1 -0
  120. package/dist/components/templated-text-kwargs.d.ts +37 -0
  121. package/dist/components/templated-text-kwargs.d.ts.map +1 -0
  122. package/dist/components/templated-text-kwargs.js +66 -0
  123. package/dist/components/templated-text-kwargs.js.map +1 -0
  124. package/dist/components/templated-text-source.d.ts +33 -0
  125. package/dist/components/templated-text-source.d.ts.map +1 -0
  126. package/dist/components/templated-text-source.js +53 -0
  127. package/dist/components/templated-text-source.js.map +1 -0
  128. package/dist/components/tool-picker-parts.d.ts +39 -0
  129. package/dist/components/tool-picker-parts.d.ts.map +1 -0
  130. package/dist/components/tool-picker-parts.js +87 -0
  131. package/dist/components/tool-picker-parts.js.map +1 -0
  132. package/dist/components/tool-picker.d.ts +1 -2
  133. package/dist/components/tool-picker.d.ts.map +1 -1
  134. package/dist/components/tool-picker.js +4 -66
  135. package/dist/components/tool-picker.js.map +1 -1
  136. package/dist/components/version-history-panel.d.ts.map +1 -1
  137. package/dist/components/version-history-panel.js +7 -34
  138. package/dist/components/version-history-panel.js.map +1 -1
  139. package/dist/components/version-history-rows.d.ts +48 -0
  140. package/dist/components/version-history-rows.d.ts.map +1 -0
  141. package/dist/components/version-history-rows.js +51 -0
  142. package/dist/components/version-history-rows.js.map +1 -0
  143. package/dist/guards.d.ts +6 -0
  144. package/dist/guards.d.ts.map +1 -0
  145. package/dist/guards.js +8 -0
  146. package/dist/guards.js.map +1 -0
  147. package/dist/hooks/interactions-connection.d.ts +43 -0
  148. package/dist/hooks/interactions-connection.d.ts.map +1 -0
  149. package/dist/hooks/interactions-connection.js +171 -0
  150. package/dist/hooks/interactions-connection.js.map +1 -0
  151. package/dist/hooks/interactions-stream.d.ts +80 -0
  152. package/dist/hooks/interactions-stream.d.ts.map +1 -0
  153. package/dist/hooks/interactions-stream.js +233 -0
  154. package/dist/hooks/interactions-stream.js.map +1 -0
  155. package/dist/hooks/useApi.d.ts +2409 -2293
  156. package/dist/hooks/useApi.d.ts.map +1 -1
  157. package/dist/hooks/useSse.d.ts +3 -6
  158. package/dist/hooks/useSse.d.ts.map +1 -1
  159. package/dist/hooks/useSse.js +38 -451
  160. package/dist/hooks/useSse.js.map +1 -1
  161. package/dist/navigation/app-link.d.ts +25 -0
  162. package/dist/navigation/app-link.d.ts.map +1 -0
  163. package/dist/navigation/app-link.js +33 -0
  164. package/dist/navigation/app-link.js.map +1 -0
  165. package/dist/navigation/context.d.ts +3 -20
  166. package/dist/navigation/context.d.ts.map +1 -1
  167. package/dist/navigation/context.js +7 -267
  168. package/dist/navigation/context.js.map +1 -1
  169. package/dist/navigation/entry-state.d.ts +23 -0
  170. package/dist/navigation/entry-state.d.ts.map +1 -0
  171. package/dist/navigation/entry-state.js +39 -0
  172. package/dist/navigation/entry-state.js.map +1 -0
  173. package/dist/navigation/guard-registry.d.ts +30 -0
  174. package/dist/navigation/guard-registry.d.ts.map +1 -0
  175. package/dist/navigation/guard-registry.js +46 -0
  176. package/dist/navigation/guard-registry.js.map +1 -0
  177. package/dist/navigation/guarded-navigation.d.ts +24 -0
  178. package/dist/navigation/guarded-navigation.d.ts.map +1 -0
  179. package/dist/navigation/guarded-navigation.js +102 -0
  180. package/dist/navigation/guarded-navigation.js.map +1 -0
  181. package/dist/navigation/index.d.ts +3 -2
  182. package/dist/navigation/index.d.ts.map +1 -1
  183. package/dist/navigation/index.js +2 -1
  184. package/dist/navigation/index.js.map +1 -1
  185. package/dist/navigation/types.d.ts +8 -11
  186. package/dist/navigation/types.d.ts.map +1 -1
  187. package/dist/navigation/use-back-forward-guard.d.ts +5 -0
  188. package/dist/navigation/use-back-forward-guard.d.ts.map +1 -0
  189. package/dist/navigation/use-back-forward-guard.js +82 -0
  190. package/dist/navigation/use-back-forward-guard.js.map +1 -0
  191. package/dist/plugin/registry.d.ts +6 -15
  192. package/dist/plugin/registry.d.ts.map +1 -1
  193. package/dist/plugin/registry.js +83 -75
  194. package/dist/plugin/registry.js.map +1 -1
  195. package/dist/schema-editor/SchemaEditor.d.ts +8 -1
  196. package/dist/schema-editor/SchemaEditor.d.ts.map +1 -1
  197. package/dist/schema-editor/SchemaEditor.js +2 -2
  198. package/dist/schema-editor/SchemaEditor.js.map +1 -1
  199. package/dist/schema-form/SecretRefField.d.ts +2 -3
  200. package/dist/schema-form/SecretRefField.d.ts.map +1 -1
  201. package/dist/schema-form/SecretRefField.js +23 -12
  202. package/dist/schema-form/SecretRefField.js.map +1 -1
  203. package/dist/schema-form/classify.d.ts +1 -122
  204. package/dist/schema-form/classify.d.ts.map +1 -1
  205. package/dist/schema-form/classify.js +174 -237
  206. package/dist/schema-form/classify.js.map +1 -1
  207. package/dist/schema-form/default-value.d.ts.map +1 -1
  208. package/dist/schema-form/default-value.js +42 -19
  209. package/dist/schema-form/default-value.js.map +1 -1
  210. package/dist/schema-form/enum-field.d.ts +1 -1
  211. package/dist/schema-form/enum-field.d.ts.map +1 -1
  212. package/dist/schema-form/field-model.d.ts +121 -0
  213. package/dist/schema-form/field-model.d.ts.map +1 -0
  214. package/dist/schema-form/field-model.js +2 -0
  215. package/dist/schema-form/field-model.js.map +1 -0
  216. package/dist/schema-form/field-node.d.ts.map +1 -1
  217. package/dist/schema-form/field-node.js +25 -14
  218. package/dist/schema-form/field-node.js.map +1 -1
  219. package/dist/schema-form/media-field.d.ts +1 -1
  220. package/dist/schema-form/media-field.d.ts.map +1 -1
  221. package/dist/schema-form/media-field.js +47 -28
  222. package/dist/schema-form/media-field.js.map +1 -1
  223. package/dist/schema-form/string-annotations.d.ts +20 -0
  224. package/dist/schema-form/string-annotations.d.ts.map +1 -0
  225. package/dist/schema-form/string-annotations.js +105 -0
  226. package/dist/schema-form/string-annotations.js.map +1 -0
  227. package/dist/schema-form/string-field.d.ts +1 -1
  228. package/dist/schema-form/string-field.d.ts.map +1 -1
  229. package/dist/schema-form/union-field.d.ts +1 -1
  230. package/dist/schema-form/union-field.d.ts.map +1 -1
  231. package/dist/schema-form/union-field.js.map +1 -1
  232. package/dist/schema-form/union.d.ts +1 -1
  233. package/dist/schema-form/union.d.ts.map +1 -1
  234. package/dist/schema-form/validate.d.ts.map +1 -1
  235. package/dist/schema-form/validate.js +126 -105
  236. package/dist/schema-form/validate.js.map +1 -1
  237. package/dist/state-binding/AdapterMapping.d.ts.map +1 -1
  238. package/dist/state-binding/AdapterMapping.js +0 -0
  239. package/dist/state-binding/AdapterMapping.js.map +1 -1
  240. package/dist/state-binding/StateAttachRow.d.ts.map +1 -1
  241. package/dist/state-binding/StateAttachRow.js +52 -36
  242. package/dist/state-binding/StateAttachRow.js.map +1 -1
  243. package/dist/state-binding/adapter-mapping-row.d.ts +17 -0
  244. package/dist/state-binding/adapter-mapping-row.d.ts.map +1 -0
  245. package/dist/state-binding/adapter-mapping-row.js +87 -0
  246. package/dist/state-binding/adapter-mapping-row.js.map +1 -0
  247. package/dist/structured-output/StructuredOutput.d.ts.map +1 -1
  248. package/dist/structured-output/StructuredOutput.js +1 -3
  249. package/dist/structured-output/StructuredOutput.js.map +1 -1
  250. package/package.json +2 -2
  251. package/src/components/explorer-body.tsx +239 -0
  252. package/src/components/explorer-page-size.ts +39 -0
  253. package/src/components/explorer-tag-filter.tsx +80 -0
  254. package/src/components/explorer-tags.ts +64 -0
  255. package/src/components/explorer-view.tsx +114 -366
  256. package/src/components/extension-combo-builder.tsx +35 -152
  257. package/src/components/extension-combo-parts.tsx +237 -0
  258. package/src/components/fleet-report.tsx +97 -54
  259. package/src/components/icons/chrome-marks.tsx +195 -0
  260. package/src/components/icons/icon-frame.tsx +50 -0
  261. package/src/components/icons/index.ts +84 -0
  262. package/src/components/icons/nav-map.ts +55 -0
  263. package/src/components/icons/nav-marks.tsx +205 -0
  264. package/src/components/icons/status-marks.tsx +46 -0
  265. package/src/components/icons/theme-marks.tsx +33 -0
  266. package/src/components/json-diff.tsx +1 -4
  267. package/src/components/json-tree-context.ts +26 -0
  268. package/src/components/json-tree-model.ts +139 -0
  269. package/src/components/json-tree-nodes.tsx +224 -0
  270. package/src/components/json-tree.tsx +15 -434
  271. package/src/components/json-value-format.ts +64 -0
  272. package/src/components/markdown-blocks.ts +198 -0
  273. package/src/components/markdown-inline.tsx +365 -0
  274. package/src/components/markdown.tsx +5 -471
  275. package/src/components/observer-registry.ts +139 -0
  276. package/src/components/overflow-measure.ts +69 -0
  277. package/src/components/prose-regions.ts +153 -0
  278. package/src/components/scroll-region.tsx +20 -360
  279. package/src/components/tab-stop-parse.ts +217 -0
  280. package/src/components/templated-text-field.tsx +38 -330
  281. package/src/components/templated-text-kwargs-editor.tsx +147 -0
  282. package/src/components/templated-text-kwargs.ts +92 -0
  283. package/src/components/templated-text-source.tsx +226 -0
  284. package/src/components/tool-picker-parts.tsx +184 -0
  285. package/src/components/tool-picker.tsx +22 -114
  286. package/src/components/version-history-panel.tsx +38 -142
  287. package/src/components/version-history-rows.tsx +220 -0
  288. package/src/guards.ts +7 -0
  289. package/src/hooks/interactions-connection.ts +227 -0
  290. package/src/hooks/interactions-stream.ts +274 -0
  291. package/src/hooks/useSse.ts +50 -477
  292. package/src/navigation/app-link.tsx +70 -0
  293. package/src/navigation/context.tsx +11 -330
  294. package/src/navigation/entry-state.ts +42 -0
  295. package/src/navigation/guard-registry.ts +65 -0
  296. package/src/navigation/guarded-navigation.ts +147 -0
  297. package/src/navigation/index.ts +2 -2
  298. package/src/navigation/types.ts +8 -11
  299. package/src/navigation/use-back-forward-guard.ts +90 -0
  300. package/src/plugin/registry.ts +111 -90
  301. package/src/schema-editor/SchemaEditor.tsx +9 -1
  302. package/src/schema-form/SecretRefField.tsx +121 -71
  303. package/src/schema-form/classify.ts +244 -395
  304. package/src/schema-form/default-value.ts +48 -17
  305. package/src/schema-form/enum-field.tsx +1 -1
  306. package/src/schema-form/field-model.ts +133 -0
  307. package/src/schema-form/field-node.tsx +94 -38
  308. package/src/schema-form/media-field.tsx +134 -65
  309. package/src/schema-form/string-annotations.ts +111 -0
  310. package/src/schema-form/string-field.tsx +1 -1
  311. package/src/schema-form/union-field.tsx +2 -1
  312. package/src/schema-form/union.ts +1 -1
  313. package/src/schema-form/validate.ts +184 -103
  314. package/src/state-binding/AdapterMapping.tsx +0 -0
  315. package/src/state-binding/StateAttachRow.tsx +222 -147
  316. package/src/state-binding/adapter-mapping-row.tsx +232 -0
  317. package/src/structured-output/StructuredOutput.tsx +1 -4
  318. package/dist/components/icons.d.ts +0 -160
  319. package/dist/components/icons.d.ts.map +0 -1
  320. package/dist/components/icons.js +0 -160
  321. package/dist/components/icons.js.map +0 -1
  322. package/src/components/icons.tsx +0 -586
@@ -16,8 +16,11 @@
16
16
  * empty value so the committed seed validates against its own schema.
17
17
  */
18
18
  import { classifySchema } from './classify';
19
+ import type { FieldModel } from './field-model';
19
20
  import type { JsonSchema } from './types';
20
21
 
22
+ type ModelOf<K extends FieldModel['kind']> = Extract<FieldModel, { kind: K }>;
23
+
21
24
  /** The explicit `default` (wrapper takes precedence over the resolved node), if any. */
22
25
  function explicitDefault(
23
26
  raw: JsonSchema,
@@ -52,6 +55,48 @@ export function skeletonValueForSchema(schema: JsonSchema, root: JsonSchema = sc
52
55
  return seedValue(schema, root, true);
53
56
  }
54
57
 
58
+ /**
59
+ * Seed an object's required nested structure and any optional property that
60
+ * carries an explicit default. A required property recurses; an optional one is
61
+ * materialized only when it declares a `default`, so its own required children
62
+ * never raise errors on a field the user did not opt into.
63
+ */
64
+ function seedObject(
65
+ model: ModelOf<'object'>,
66
+ root: JsonSchema,
67
+ mintScalars: boolean,
68
+ ): Record<string, unknown> {
69
+ const result: Record<string, unknown> = {};
70
+ for (const [name, propSchema] of model.properties) {
71
+ if (model.required.has(name)) {
72
+ const seeded = seedValue(propSchema, root, mintScalars);
73
+ if (seeded !== undefined) result[name] = seeded;
74
+ continue;
75
+ }
76
+ const propClassified = classifySchema(propSchema, root);
77
+ const propDefault = explicitDefault(propSchema, propClassified.schema);
78
+ if (propDefault.present) result[name] = propDefault.value;
79
+ }
80
+ return result;
81
+ }
82
+
83
+ /**
84
+ * The empty value a required scalar mints in a committed skeleton: strings `''`,
85
+ * numbers `0`, booleans `false`. Absent (left value-less) on the default path,
86
+ * where a fresh form should flag the missing required scalar instead.
87
+ */
88
+ function seedScalar(kind: 'string' | 'number' | 'boolean', mintScalars: boolean): unknown {
89
+ if (!mintScalars) return undefined;
90
+ switch (kind) {
91
+ case 'string':
92
+ return '';
93
+ case 'number':
94
+ return 0;
95
+ case 'boolean':
96
+ return false;
97
+ }
98
+ }
99
+
55
100
  /**
56
101
  * The shared seeding walk. `mintScalars` distinguishes the two entry points
57
102
  * above: `false` leaves value-less required scalars absent; `true` mints their
@@ -66,30 +111,16 @@ function seedValue(schema: JsonSchema, root: JsonSchema, mintScalars: boolean):
66
111
  switch (model.kind) {
67
112
  case 'const':
68
113
  return model.value;
69
- case 'object': {
70
- const result: Record<string, unknown> = {};
71
- for (const [name, propSchema] of model.properties) {
72
- if (model.required.has(name)) {
73
- const seeded = seedValue(propSchema, root, mintScalars);
74
- if (seeded !== undefined) result[name] = seeded;
75
- continue;
76
- }
77
- const propClassified = classifySchema(propSchema, root);
78
- const propDefault = explicitDefault(propSchema, propClassified.schema);
79
- if (propDefault.present) result[name] = propDefault.value;
80
- }
81
- return result;
82
- }
114
+ case 'object':
115
+ return seedObject(model, root, mintScalars);
83
116
  case 'array':
84
117
  return [];
85
118
  case 'record':
86
119
  return {};
87
120
  case 'string':
88
- return mintScalars ? '' : undefined;
89
121
  case 'number':
90
- return mintScalars ? 0 : undefined;
91
122
  case 'boolean':
92
- return mintScalars ? false : undefined;
123
+ return seedScalar(model.kind, mintScalars);
93
124
  case 'enum':
94
125
  case 'union':
95
126
  case 'json':
@@ -9,7 +9,7 @@ import type { ReactNode } from 'react';
9
9
  import { Field } from '../components/field';
10
10
  import { RadioGroup } from '../components/radio-group';
11
11
  import { Select } from '../components/select';
12
- import type { FieldModel } from './classify';
12
+ import type { FieldModel } from './field-model';
13
13
 
14
14
  const RADIO_MAX_OPTIONS = 3;
15
15
 
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The renderer-facing result types produced by `classifySchema`: the discriminated
3
+ * `FieldModel`, the `ClassifiedField` envelope, and the annotation/variant shapes
4
+ * they carry. Kept apart from the classifier logic so both the renderer and the
5
+ * validator import the vocabulary without pulling in classification code.
6
+ */
7
+ import type { EnumOption } from './resolve';
8
+ import type { JsonSchema } from './types';
9
+
10
+ /**
11
+ * How a string field accepts binary content as a JSON-serializable string.
12
+ * Derived from a field's JSON-Schema media annotations (see the `SchemaForm` module).
13
+ */
14
+ export interface MediaUpload {
15
+ /**
16
+ * `base64` — the emitted value is the raw base64 body (no `data:` prefix),
17
+ * matching `contentEncoding: "base64"`. `data-url` — the emitted value is a
18
+ * full `data:<mime>;base64,<body>` URL, matching `format: "data-url"`.
19
+ */
20
+ readonly encoding: 'base64' | 'data-url';
21
+ /**
22
+ * The `contentMediaType` constraint (e.g. `image/*`, `application/pdf`), used
23
+ * for MIME validation and the file picker's `accept`. `undefined` for a bare
24
+ * `format: "data-url"` field with no declared type.
25
+ */
26
+ readonly mediaType: string | undefined;
27
+ /** A per-field byte cap from `contentMaxBytes`, if the schema pins one. */
28
+ readonly maxBytes: number | undefined;
29
+ }
30
+
31
+ /** One top-level key of the expression's input document, with a one-line gloss. */
32
+ export interface ExpressionAnnotationKey {
33
+ readonly name: string;
34
+ readonly gloss: string;
35
+ }
36
+
37
+ /**
38
+ * The `x-tai42-expression` annotation on a string schema: the server declares
39
+ * that the field's value is authored in a pipeline expression language (only jq
40
+ * today) and optionally describes what `.` is for that expression. The renderer
41
+ * uses it to open the visual-editor door instead of a plain text box. The shape
42
+ * mirrors jq-studio's input-shape descriptor WITHOUT importing it — classification
43
+ * stays a pure schema concern; the renderer maps this onto a jq field declaration.
44
+ */
45
+ export interface ExpressionAnnotation {
46
+ /** The declared language. An unknown language never reaches here — it classifies
47
+ * as a plain string, so an older client renders a newer server's field as the
48
+ * ordinary text input it always was. */
49
+ readonly language: 'jq';
50
+ /** Short chip label for what `.` is (e.g. "node envelope"). */
51
+ readonly label: string | undefined;
52
+ /** One sentence describing what `.` is in this field. */
53
+ readonly blurb: string | undefined;
54
+ /** The top-level keys of `.`, each with a one-liner. */
55
+ readonly keys: readonly ExpressionAnnotationKey[] | undefined;
56
+ /** What the expression must return (e.g. "true or false"). */
57
+ readonly returns: string | undefined;
58
+ /** Per-field caveats. */
59
+ readonly caveats: readonly string[] | undefined;
60
+ /** True when the annotation carries a `sample` — tracked separately because a
61
+ * legitimate sample may be JSON `null`. */
62
+ readonly hasSample: boolean;
63
+ /** A static skeleton of `.` for the Test panel. Meaningful only when
64
+ * {@link hasSample} is true. */
65
+ readonly sample: unknown;
66
+ }
67
+
68
+ /** One selectable variant of a union field. */
69
+ export interface UnionVariant {
70
+ readonly label: string;
71
+ /** The discriminator tag value, or `undefined` for a non-discriminated union. */
72
+ readonly tag: unknown;
73
+ /** The variant schema as written (unresolved — the renderer resolves it). */
74
+ readonly schema: JsonSchema;
75
+ }
76
+
77
+ /** The renderer-facing classification of a schema node. */
78
+ export type FieldModel =
79
+ | { readonly kind: 'const'; readonly value: unknown }
80
+ | { readonly kind: 'enum'; readonly options: readonly EnumOption[] }
81
+ | {
82
+ readonly kind: 'string';
83
+ readonly format: string | undefined;
84
+ /** Present when media annotations opt the field into the upload control. */
85
+ readonly media: MediaUpload | undefined;
86
+ /** Present when a well-formed `x-tai42-expression` annotation opts the
87
+ * field into the jq expression editor (see {@link ExpressionAnnotation}). */
88
+ readonly expression: ExpressionAnnotation | undefined;
89
+ }
90
+ | { readonly kind: 'number'; readonly integer: boolean }
91
+ | { readonly kind: 'boolean' }
92
+ | { readonly kind: 'array'; readonly items: JsonSchema }
93
+ | {
94
+ readonly kind: 'object';
95
+ readonly properties: readonly (readonly [string, JsonSchema])[];
96
+ readonly required: ReadonlySet<string>;
97
+ }
98
+ | {
99
+ // A string-keyed map typed by `additionalProperties` (no fixed
100
+ // `properties`), edited as key/value rows. `values` is the entry value
101
+ // schema AS WRITTEN — the entry editor resolves any `$ref` itself, exactly
102
+ // as `array.items` is.
103
+ readonly kind: 'record';
104
+ readonly values: JsonSchema;
105
+ }
106
+ | {
107
+ readonly kind: 'union';
108
+ readonly variants: readonly UnionVariant[];
109
+ readonly discriminator: string | undefined;
110
+ }
111
+ | {
112
+ // A shape with no structured editor to build — a property-less object, a
113
+ // bare `additionalProperties`-open object, an items-less array, an empty/
114
+ // multi-type/`allOf` schema. It is NOT undroppable: every one of these still
115
+ // describes JSON, so the renderer edits it as free-form JSON in a mono
116
+ // textarea rather than dead-ending on a badge. `jsonType` is the container
117
+ // the schema commits to, so a valid buffer must parse to it (`'any'` = no
118
+ // constraint beyond "is JSON"); `reason` records which shape fell through.
119
+ readonly kind: 'json';
120
+ readonly jsonType: 'object' | 'array' | 'any';
121
+ readonly reason: string;
122
+ };
123
+
124
+ /** A fully classified field: its model plus resolved metadata. */
125
+ export interface ClassifiedField {
126
+ readonly model: FieldModel;
127
+ /** True when JSON `null` is an accepted value (a null-union member). */
128
+ readonly nullable: boolean;
129
+ /** The effective (ref-resolved) schema this classification describes. */
130
+ readonly schema: JsonSchema;
131
+ readonly title: string | undefined;
132
+ readonly description: string | undefined;
133
+ }
@@ -47,24 +47,12 @@ function labelText(explicit: string | undefined, label: string | undefined): str
47
47
 
48
48
  /** A single schema node, dispatched on its classified model kind. */
49
49
  export function FieldNode(props: FieldNodeProps): ReactNode {
50
- const {
51
- schema,
52
- root,
53
- value,
54
- onChange,
55
- path,
56
- label,
57
- required,
58
- markRequired = false,
59
- errors,
60
- idPrefix,
61
- } = props;
62
- const classified = classifySchema(schema, root);
63
- const base = labelText(classified.title, label);
64
- const heading = markRequired && required ? `${base} *` : base;
65
- const description = classified.description;
50
+ const { root, value, onChange, path, required, idPrefix, errors } = props;
51
+ const classified = classifySchema(props.schema, root);
52
+ const base = labelText(classified.title, props.label);
53
+ const heading = props.markRequired === true && required ? `${base} *` : base;
54
+ const { model, description } = classified;
66
55
  const error = errors?.[path];
67
- const model = classified.model;
68
56
 
69
57
  switch (model.kind) {
70
58
  case 'json':
@@ -81,9 +69,12 @@ export function FieldNode(props: FieldNodeProps): ReactNode {
81
69
  );
82
70
  case 'const':
83
71
  return (
84
- <Field label={heading} description={description} error={error}>
85
- <TextInput value={scalarLabel(model.value)} readOnly disabled />
86
- </Field>
72
+ <ConstField
73
+ heading={heading}
74
+ description={description}
75
+ error={error}
76
+ constValue={model.value}
77
+ />
87
78
  );
88
79
  case 'enum':
89
80
  return (
@@ -113,27 +104,24 @@ export function FieldNode(props: FieldNodeProps): ReactNode {
113
104
  );
114
105
  case 'number':
115
106
  return (
116
- <Field label={heading} description={description} error={error}>
117
- <NumberInput
118
- step={model.integer ? '1' : 'any'}
119
- value={typeof value === 'number' ? value : ''}
120
- onChange={(event) => {
121
- const raw = event.target.value;
122
- onChange(raw === '' ? undefined : Number(raw));
123
- }}
124
- />
125
- </Field>
107
+ <NumberField
108
+ heading={heading}
109
+ description={description}
110
+ error={error}
111
+ integer={model.integer}
112
+ value={value}
113
+ onChange={onChange}
114
+ />
126
115
  );
127
116
  case 'boolean':
128
117
  return (
129
- <Field label={heading} description={description} error={error}>
130
- <Checkbox
131
- checked={value === true}
132
- onCheckedChange={(checked) => {
133
- onChange(checked);
134
- }}
135
- />
136
- </Field>
118
+ <BooleanField
119
+ heading={heading}
120
+ description={description}
121
+ error={error}
122
+ value={value}
123
+ onChange={onChange}
124
+ />
137
125
  );
138
126
  case 'array':
139
127
  return (
@@ -198,3 +186,71 @@ export function FieldNode(props: FieldNodeProps): ReactNode {
198
186
  );
199
187
  }
200
188
  }
189
+
190
+ /** The label/description/error chrome every scalar leaf field shares. */
191
+ interface ScalarFieldChrome {
192
+ readonly heading: string;
193
+ readonly description: string | undefined;
194
+ readonly error: string | undefined;
195
+ }
196
+
197
+ /** A `const`-pinned value: shown read-only, never editable. */
198
+ function ConstField({
199
+ heading,
200
+ description,
201
+ error,
202
+ constValue,
203
+ }: ScalarFieldChrome & { constValue: unknown }): ReactNode {
204
+ return (
205
+ <Field label={heading} description={description} error={error}>
206
+ <TextInput value={scalarLabel(constValue)} readOnly disabled />
207
+ </Field>
208
+ );
209
+ }
210
+
211
+ /** A number/integer field; an empty input clears the value to `undefined`. */
212
+ function NumberField({
213
+ heading,
214
+ description,
215
+ error,
216
+ integer,
217
+ value,
218
+ onChange,
219
+ }: ScalarFieldChrome & {
220
+ integer: boolean;
221
+ value: unknown;
222
+ onChange: (value: unknown) => void;
223
+ }): ReactNode {
224
+ return (
225
+ <Field label={heading} description={description} error={error}>
226
+ <NumberInput
227
+ step={integer ? '1' : 'any'}
228
+ value={typeof value === 'number' ? value : ''}
229
+ onChange={(event) => {
230
+ const raw = event.target.value;
231
+ onChange(raw === '' ? undefined : Number(raw));
232
+ }}
233
+ />
234
+ </Field>
235
+ );
236
+ }
237
+
238
+ /** A boolean field rendered as a checkbox. */
239
+ function BooleanField({
240
+ heading,
241
+ description,
242
+ error,
243
+ value,
244
+ onChange,
245
+ }: ScalarFieldChrome & { value: unknown; onChange: (value: unknown) => void }): ReactNode {
246
+ return (
247
+ <Field label={heading} description={description} error={error}>
248
+ <Checkbox
249
+ checked={value === true}
250
+ onCheckedChange={(checked) => {
251
+ onChange(checked);
252
+ }}
253
+ />
254
+ </Field>
255
+ );
256
+ }
@@ -13,7 +13,7 @@ import { XCircleIcon } from '../components/icons';
13
13
  import { Button } from '../components/primitives';
14
14
  import { Field, useFieldControl, type FieldControlProps } from '../components/field';
15
15
  import { TextInput } from '../components/inputs';
16
- import type { MediaUpload } from './classify';
16
+ import type { MediaUpload } from './field-model';
17
17
  import { MaxUploadBytesContext } from './context';
18
18
  import { decodedByteSize, effectiveMaxBytes, overCapMessage } from './media';
19
19
 
@@ -114,9 +114,7 @@ export function MediaField({
114
114
  const defaultMax = useContext(MaxUploadBytesContext);
115
115
  const maxBytes = effectiveMaxBytes(media.maxBytes, defaultMax);
116
116
  const [uploadError, setUploadError] = useState<string | undefined>(undefined);
117
- const [attached, setAttached] = useState<
118
- { name: string; previewUrl: string; value: string } | undefined
119
- >(undefined);
117
+ const [attached, setAttached] = useState<AttachedMedia | undefined>(undefined);
120
118
 
121
119
  const accept = (file: File | undefined): void => {
122
120
  if (file === undefined) return;
@@ -139,19 +137,7 @@ export function MediaField({
139
137
  onChange('');
140
138
  };
141
139
 
142
- // The just-picked file's chip/preview applies ONLY while it still matches the
143
- // current field value; if the value is reset or replaced externally (an edit
144
- // loads a different record, or the form resets after submit), fall back to a
145
- // value-derived display so a stale filename/thumbnail can never linger.
146
- const activeAttached = attached?.value === value ? attached : undefined;
147
- // Prefer the just-read preview; on an edit with a pre-existing data-url value,
148
- // reconstruct the preview from the value itself.
149
- const previewUrl =
150
- activeAttached?.previewUrl ??
151
- (media.encoding === 'data-url' && value.startsWith('data:') ? value : undefined);
152
- const isImage =
153
- media.mediaType?.startsWith('image/') === true || previewUrl?.startsWith('data:image') === true;
154
- const fileName = activeAttached?.name ?? (value !== '' ? 'Attached file' : undefined);
140
+ const { previewUrl, isImage, fileName } = mediaPreview(attached, value, media);
155
141
 
156
142
  return (
157
143
  <Field label={heading} description={description} error={error}>
@@ -161,29 +147,12 @@ export function MediaField({
161
147
  onFile={accept}
162
148
  />
163
149
 
164
- {fileName !== undefined ? (
165
- <div className="tai-row">
166
- {isImage && previewUrl !== undefined ? (
167
- <img
168
- src={previewUrl}
169
- alt={fileName}
170
- // A thumbnail is per-instance geometry: it is capped to a fixed
171
- // height and can never exceed the width it is given.
172
- style={{
173
- maxWidth: '100%',
174
- maxHeight: '6rem',
175
- borderRadius: 'var(--tai-radius-md)',
176
- border: '1px solid var(--tai-color-control-border)',
177
- }}
178
- />
179
- ) : (
180
- <Badge>{fileName}</Badge>
181
- )}
182
- <Button type="button" variant="secondary" onClick={clear}>
183
- Remove
184
- </Button>
185
- </div>
186
- ) : null}
150
+ <AttachedPreview
151
+ fileName={fileName}
152
+ isImage={isImage}
153
+ previewUrl={previewUrl}
154
+ onClear={clear}
155
+ />
187
156
 
188
157
  {uploadError !== undefined ? (
189
158
  <span id={uploadErrorId} role="alert" className="tai-field-error">
@@ -192,33 +161,133 @@ export function MediaField({
192
161
  </span>
193
162
  ) : null}
194
163
 
195
- <label htmlFor={pasteId} className="tai-field">
196
- <span className="tai-field-hint">Or paste a value</span>
197
- <TextInput
198
- id={pasteId}
199
- type="text"
200
- // Only the upload error's id: `TextInput` joins it with the enclosing
201
- // Field's own description and error IDREFs rather than replacing them.
202
- aria-describedby={uploadError === undefined ? undefined : uploadErrorId}
203
- aria-invalid={uploadError === undefined ? undefined : true}
204
- value={value}
205
- onChange={(event) => {
206
- const next = event.target.value;
207
- // The same effective cap the picker enforces, measured on the pasted
208
- // string's DECODED byte size. Reject over-cap LOUDLY; never accept or
209
- // truncate — the field keeps its prior value.
210
- const size = decodedByteSize(next);
211
- if (size > maxBytes) {
212
- setUploadError(overCapMessage('The pasted value', size, maxBytes));
213
- return;
214
- }
215
- setUploadError(undefined);
216
- setAttached(undefined);
217
- onChange(next);
164
+ <MediaPasteInput
165
+ pasteId={pasteId}
166
+ value={value}
167
+ maxBytes={maxBytes}
168
+ uploadErrorId={uploadError === undefined ? undefined : uploadErrorId}
169
+ onReject={setUploadError}
170
+ onAccept={(next) => {
171
+ setUploadError(undefined);
172
+ setAttached(undefined);
173
+ onChange(next);
174
+ }}
175
+ />
176
+ </Field>
177
+ );
178
+ }
179
+
180
+ /** The just-picked file, retained only while it still matches the current field value. */
181
+ interface AttachedMedia {
182
+ name: string;
183
+ previewUrl: string;
184
+ value: string;
185
+ }
186
+
187
+ /**
188
+ * The display derived from the attached file and the current value: the chip/preview
189
+ * applies ONLY while the attachment still matches the value (an external reset or a
190
+ * different record loading falls back to a value-derived display so a stale
191
+ * filename/thumbnail can never linger); an existing data-url value reconstructs its
192
+ * own preview.
193
+ */
194
+ function mediaPreview(
195
+ attached: AttachedMedia | undefined,
196
+ value: string,
197
+ media: MediaUpload,
198
+ ): { previewUrl: string | undefined; isImage: boolean; fileName: string | undefined } {
199
+ const activeAttached = attached?.value === value ? attached : undefined;
200
+ const previewUrl =
201
+ activeAttached?.previewUrl ??
202
+ (media.encoding === 'data-url' && value.startsWith('data:') ? value : undefined);
203
+ const isImage =
204
+ media.mediaType?.startsWith('image/') === true || previewUrl?.startsWith('data:image') === true;
205
+ const fileName = activeAttached?.name ?? (value !== '' ? 'Attached file' : undefined);
206
+ return { previewUrl, isImage, fileName };
207
+ }
208
+
209
+ /** The attached-file chip: an image thumbnail when previewable, else a filename badge, plus Remove. */
210
+ function AttachedPreview({
211
+ fileName,
212
+ isImage,
213
+ previewUrl,
214
+ onClear,
215
+ }: {
216
+ fileName: string | undefined;
217
+ isImage: boolean;
218
+ previewUrl: string | undefined;
219
+ onClear: () => void;
220
+ }): ReactNode {
221
+ if (fileName === undefined) return null;
222
+ return (
223
+ <div className="tai-row">
224
+ {isImage && previewUrl !== undefined ? (
225
+ <img
226
+ src={previewUrl}
227
+ alt={fileName}
228
+ // A thumbnail is per-instance geometry: it is capped to a fixed height and
229
+ // can never exceed the width it is given.
230
+ style={{
231
+ maxWidth: '100%',
232
+ maxHeight: '6rem',
233
+ borderRadius: 'var(--tai-radius-md)',
234
+ border: '1px solid var(--tai-color-control-border)',
218
235
  }}
219
236
  />
220
- </label>
221
- </Field>
237
+ ) : (
238
+ <Badge>{fileName}</Badge>
239
+ )}
240
+ <Button type="button" variant="secondary" onClick={onClear}>
241
+ Remove
242
+ </Button>
243
+ </div>
244
+ );
245
+ }
246
+
247
+ /**
248
+ * The paste fallback: a text input that enforces the SAME effective byte cap the
249
+ * picker does, measured on the pasted string's DECODED size. An over-cap paste is
250
+ * rejected LOUDLY (`onReject`) and the field keeps its prior value; an accepted paste
251
+ * clears any attachment and emits the new value (`onAccept`).
252
+ */
253
+ function MediaPasteInput({
254
+ pasteId,
255
+ value,
256
+ maxBytes,
257
+ uploadErrorId,
258
+ onReject,
259
+ onAccept,
260
+ }: {
261
+ pasteId: string;
262
+ value: string;
263
+ maxBytes: number;
264
+ /** Set while an upload error stands, so the input points at it and reads invalid. */
265
+ uploadErrorId: string | undefined;
266
+ onReject: (message: string) => void;
267
+ onAccept: (value: string) => void;
268
+ }): ReactNode {
269
+ return (
270
+ <label htmlFor={pasteId} className="tai-field">
271
+ <span className="tai-field-hint">Or paste a value</span>
272
+ <TextInput
273
+ id={pasteId}
274
+ type="text"
275
+ // Only the upload error's id: `TextInput` joins it with the enclosing Field's
276
+ // own description and error IDREFs rather than replacing them.
277
+ aria-describedby={uploadErrorId}
278
+ aria-invalid={uploadErrorId === undefined ? undefined : true}
279
+ value={value}
280
+ onChange={(event) => {
281
+ const next = event.target.value;
282
+ const size = decodedByteSize(next);
283
+ if (size > maxBytes) {
284
+ onReject(overCapMessage('The pasted value', size, maxBytes));
285
+ return;
286
+ }
287
+ onAccept(next);
288
+ }}
289
+ />
290
+ </label>
222
291
  );
223
292
  }
224
293