@tai42/studio-sdk 30.0.1 → 31.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +98 -10
  2. package/dist/components/agent-picker.d.ts +1 -1
  3. package/dist/components/agent-picker.d.ts.map +1 -1
  4. package/dist/components/agent-picker.js +1 -0
  5. package/dist/components/agent-picker.js.map +1 -1
  6. package/dist/components/channel-picker.d.ts +1 -1
  7. package/dist/components/channel-picker.d.ts.map +1 -1
  8. package/dist/components/channel-picker.js +1 -0
  9. package/dist/components/channel-picker.js.map +1 -1
  10. package/dist/components/components.css +53 -0
  11. package/dist/components/explorer-body.d.ts +1 -0
  12. package/dist/components/explorer-body.d.ts.map +1 -1
  13. package/dist/components/explorer-body.js +6 -4
  14. package/dist/components/explorer-body.js.map +1 -1
  15. package/dist/components/explorer-view.d.ts +16 -7
  16. package/dist/components/explorer-view.d.ts.map +1 -1
  17. package/dist/components/explorer-view.js +27 -13
  18. package/dist/components/explorer-view.js.map +1 -1
  19. package/dist/components/extension-config-editor.js.map +1 -1
  20. package/dist/components/field.d.ts +6 -0
  21. package/dist/components/field.d.ts.map +1 -1
  22. package/dist/components/field.js +15 -2
  23. package/dist/components/field.js.map +1 -1
  24. package/dist/components/folder-actions.d.ts +51 -0
  25. package/dist/components/folder-actions.d.ts.map +1 -0
  26. package/dist/components/folder-actions.js +173 -0
  27. package/dist/components/folder-actions.js.map +1 -0
  28. package/dist/components/folder-filing.d.ts +32 -0
  29. package/dist/components/folder-filing.d.ts.map +1 -0
  30. package/dist/components/folder-filing.js +51 -0
  31. package/dist/components/folder-filing.js.map +1 -0
  32. package/dist/components/folder-nav.d.ts +9 -4
  33. package/dist/components/folder-nav.d.ts.map +1 -1
  34. package/dist/components/folder-nav.js +18 -22
  35. package/dist/components/folder-nav.js.map +1 -1
  36. package/dist/components/folder-occupancy.d.ts +27 -0
  37. package/dist/components/folder-occupancy.d.ts.map +1 -0
  38. package/dist/components/folder-occupancy.js +54 -0
  39. package/dist/components/folder-occupancy.js.map +1 -0
  40. package/dist/components/named-list-picker.d.ts +5 -3
  41. package/dist/components/named-list-picker.d.ts.map +1 -1
  42. package/dist/components/named-list-picker.js +7 -7
  43. package/dist/components/named-list-picker.js.map +1 -1
  44. package/dist/components/primitives.d.ts +2 -1
  45. package/dist/components/primitives.d.ts.map +1 -1
  46. package/dist/components/primitives.js +2 -1
  47. package/dist/components/primitives.js.map +1 -1
  48. package/dist/components/searchable-select-model.d.ts +40 -0
  49. package/dist/components/searchable-select-model.d.ts.map +1 -0
  50. package/dist/components/searchable-select-model.js +69 -0
  51. package/dist/components/searchable-select-model.js.map +1 -0
  52. package/dist/components/searchable-select.d.ts +38 -0
  53. package/dist/components/searchable-select.d.ts.map +1 -0
  54. package/dist/components/searchable-select.js +200 -0
  55. package/dist/components/searchable-select.js.map +1 -0
  56. package/dist/components/subject-section.d.ts.map +1 -1
  57. package/dist/components/subject-section.js +2 -2
  58. package/dist/components/subject-section.js.map +1 -1
  59. package/dist/components/table.d.ts +7 -1
  60. package/dist/components/table.d.ts.map +1 -1
  61. package/dist/components/table.js +2 -2
  62. package/dist/components/table.js.map +1 -1
  63. package/dist/components/tool-meta-access.d.ts +28 -0
  64. package/dist/components/tool-meta-access.d.ts.map +1 -0
  65. package/dist/components/tool-meta-access.js +65 -0
  66. package/dist/components/tool-meta-access.js.map +1 -0
  67. package/dist/components/tool-name-entry.d.ts +26 -0
  68. package/dist/components/tool-name-entry.d.ts.map +1 -0
  69. package/dist/components/tool-name-entry.js +43 -0
  70. package/dist/components/tool-name-entry.js.map +1 -0
  71. package/dist/components/tool-picker.d.ts +23 -18
  72. package/dist/components/tool-picker.d.ts.map +1 -1
  73. package/dist/components/tool-picker.js +51 -33
  74. package/dist/components/tool-picker.js.map +1 -1
  75. package/dist/components/tool-select.d.ts +6 -5
  76. package/dist/components/tool-select.d.ts.map +1 -1
  77. package/dist/components/tool-select.js +31 -14
  78. package/dist/components/tool-select.js.map +1 -1
  79. package/dist/hooks/useApi.d.ts +1 -0
  80. package/dist/hooks/useApi.d.ts.map +1 -1
  81. package/dist/hooks/useSystemKinds.d.ts +27 -12
  82. package/dist/hooks/useSystemKinds.d.ts.map +1 -1
  83. package/dist/hooks/useSystemKinds.js +56 -16
  84. package/dist/hooks/useSystemKinds.js.map +1 -1
  85. package/dist/index.d.ts +51 -49
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +34 -26
  88. package/dist/index.js.map +1 -1
  89. package/dist/marketplace-install/use-mount-install.d.ts +6 -6
  90. package/dist/plugin/version.d.ts +1 -1
  91. package/dist/plugin/version.js +1 -1
  92. package/dist/schema-editor/SchemaEditor.d.ts +7 -2
  93. package/dist/schema-editor/SchemaEditor.d.ts.map +1 -1
  94. package/dist/schema-editor/SchemaEditor.js +1 -1
  95. package/dist/schema-editor/SchemaEditor.js.map +1 -1
  96. package/dist/schema-editor/index.d.ts +2 -2
  97. package/dist/schema-editor/index.d.ts.map +1 -1
  98. package/dist/schema-form/array-field.d.ts.map +1 -1
  99. package/dist/schema-form/array-field.js +6 -2
  100. package/dist/schema-form/array-field.js.map +1 -1
  101. package/dist/schema-form/classify.d.ts +4 -1
  102. package/dist/schema-form/classify.d.ts.map +1 -1
  103. package/dist/schema-form/classify.js +36 -15
  104. package/dist/schema-form/classify.js.map +1 -1
  105. package/dist/schema-form/collapsed-object-field.d.ts +20 -0
  106. package/dist/schema-form/collapsed-object-field.d.ts.map +1 -0
  107. package/dist/schema-form/collapsed-object-field.js +14 -0
  108. package/dist/schema-form/collapsed-object-field.js.map +1 -0
  109. package/dist/schema-form/context.d.ts +27 -1
  110. package/dist/schema-form/context.d.ts.map +1 -1
  111. package/dist/schema-form/context.js +14 -0
  112. package/dist/schema-form/context.js.map +1 -1
  113. package/dist/schema-form/default-value.d.ts +21 -6
  114. package/dist/schema-form/default-value.d.ts.map +1 -1
  115. package/dist/schema-form/default-value.js +74 -15
  116. package/dist/schema-form/default-value.js.map +1 -1
  117. package/dist/schema-form/expression-field.d.ts +48 -0
  118. package/dist/schema-form/expression-field.d.ts.map +1 -0
  119. package/dist/schema-form/expression-field.js +88 -0
  120. package/dist/schema-form/expression-field.js.map +1 -0
  121. package/dist/schema-form/field-model.d.ts +23 -4
  122. package/dist/schema-form/field-model.d.ts.map +1 -1
  123. package/dist/schema-form/field-node.d.ts.map +1 -1
  124. package/dist/schema-form/field-node.js +16 -3
  125. package/dist/schema-form/field-node.js.map +1 -1
  126. package/dist/schema-form/index.d.ts +3 -3
  127. package/dist/schema-form/index.d.ts.map +1 -1
  128. package/dist/schema-form/index.js +2 -2
  129. package/dist/schema-form/index.js.map +1 -1
  130. package/dist/schema-form/object-fields.d.ts +6 -3
  131. package/dist/schema-form/object-fields.d.ts.map +1 -1
  132. package/dist/schema-form/object-fields.js +14 -3
  133. package/dist/schema-form/object-fields.js.map +1 -1
  134. package/dist/schema-form/record-field.d.ts.map +1 -1
  135. package/dist/schema-form/record-field.js +6 -2
  136. package/dist/schema-form/record-field.js.map +1 -1
  137. package/dist/schema-form/seeding-action.d.ts +15 -0
  138. package/dist/schema-form/seeding-action.d.ts.map +1 -0
  139. package/dist/schema-form/seeding-action.js +35 -0
  140. package/dist/schema-form/seeding-action.js.map +1 -0
  141. package/dist/schema-form/string-annotations.d.ts +22 -8
  142. package/dist/schema-form/string-annotations.d.ts.map +1 -1
  143. package/dist/schema-form/string-annotations.js +39 -14
  144. package/dist/schema-form/string-annotations.js.map +1 -1
  145. package/dist/schema-form/string-field.js +2 -84
  146. package/dist/schema-form/string-field.js.map +1 -1
  147. package/dist/schema-form/templated-field.d.ts +13 -0
  148. package/dist/schema-form/templated-field.d.ts.map +1 -0
  149. package/dist/schema-form/templated-field.js +43 -0
  150. package/dist/schema-form/templated-field.js.map +1 -0
  151. package/dist/schema-form/union-field.d.ts.map +1 -1
  152. package/dist/schema-form/union-field.js +6 -2
  153. package/dist/schema-form/union-field.js.map +1 -1
  154. package/dist/schema-form/union.d.ts +4 -4
  155. package/dist/schema-form/union.d.ts.map +1 -1
  156. package/dist/schema-form/union.js +8 -1
  157. package/dist/schema-form/union.js.map +1 -1
  158. package/dist/schema-form/validate.d.ts.map +1 -1
  159. package/dist/schema-form/validate.js +28 -0
  160. package/dist/schema-form/validate.js.map +1 -1
  161. package/dist/search-match.d.ts +29 -0
  162. package/dist/search-match.d.ts.map +1 -0
  163. package/dist/search-match.js +38 -0
  164. package/dist/search-match.js.map +1 -0
  165. package/dist/tool-organization.d.ts +15 -0
  166. package/dist/tool-organization.d.ts.map +1 -0
  167. package/dist/tool-organization.js +6 -0
  168. package/dist/tool-organization.js.map +1 -0
  169. package/package.json +3 -2
  170. package/src/components/agent-picker.tsx +2 -1
  171. package/src/components/channel-picker.tsx +2 -1
  172. package/src/components/components.css +53 -0
  173. package/src/components/explorer-body.tsx +29 -5
  174. package/src/components/explorer-view.tsx +48 -17
  175. package/src/components/extension-config-editor.tsx +3 -3
  176. package/src/components/field.tsx +18 -2
  177. package/src/components/folder-actions.tsx +341 -0
  178. package/src/components/folder-filing.tsx +109 -0
  179. package/src/components/folder-nav.tsx +26 -10
  180. package/src/components/folder-occupancy.ts +83 -0
  181. package/src/components/named-list-picker.tsx +11 -7
  182. package/src/components/primitives.tsx +2 -1
  183. package/src/components/searchable-select-model.ts +105 -0
  184. package/src/components/searchable-select.tsx +432 -0
  185. package/src/components/subject-section.tsx +3 -2
  186. package/src/components/table.tsx +12 -2
  187. package/src/components/tool-meta-access.tsx +101 -0
  188. package/src/components/tool-name-entry.tsx +77 -0
  189. package/src/components/tool-picker.tsx +83 -50
  190. package/src/components/tool-select.tsx +49 -38
  191. package/src/hooks/useSystemKinds.tsx +77 -17
  192. package/src/index.ts +98 -94
  193. package/src/plugin/version.ts +1 -1
  194. package/src/schema-editor/SchemaEditor.tsx +9 -3
  195. package/src/schema-editor/index.ts +2 -2
  196. package/src/schema-form/array-field.tsx +10 -2
  197. package/src/schema-form/classify.ts +41 -15
  198. package/src/schema-form/collapsed-object-field.tsx +56 -0
  199. package/src/schema-form/context.ts +33 -1
  200. package/src/schema-form/default-value.ts +89 -15
  201. package/src/schema-form/expression-field.tsx +162 -0
  202. package/src/schema-form/field-model.ts +27 -4
  203. package/src/schema-form/field-node.tsx +41 -2
  204. package/src/schema-form/index.ts +3 -2
  205. package/src/schema-form/object-fields.tsx +11 -4
  206. package/src/schema-form/record-field.tsx +10 -2
  207. package/src/schema-form/seeding-action.ts +44 -0
  208. package/src/schema-form/string-annotations.ts +40 -14
  209. package/src/schema-form/string-field.tsx +2 -147
  210. package/src/schema-form/templated-field.tsx +124 -0
  211. package/src/schema-form/union-field.tsx +11 -2
  212. package/src/schema-form/union.ts +12 -5
  213. package/src/schema-form/validate.ts +28 -0
  214. package/src/search-match.ts +41 -0
  215. package/src/tool-organization.ts +34 -0
@@ -12,8 +12,9 @@
12
12
  * opted into.
13
13
  *
14
14
  * `skeletonValueForSchema` is the sibling entry for programmatic reseeds (a
15
- * union variant switch): same walk, but required scalars mint their type's
16
- * empty value so the committed seed validates against its own schema.
15
+ * union variant switch): same walk, but every required field with a known empty
16
+ * value is minted, so the committed seed has no missing required field. A minted
17
+ * empty value may still fail its own value check (see the sibling's docstring).
17
18
  */
18
19
  import { classifySchema } from './classify';
19
20
  import type { FieldModel } from './field-model';
@@ -37,22 +38,56 @@ function explicitDefault(
37
38
  * whole-tool-schema call).
38
39
  */
39
40
  export function defaultValueForSchema(schema: JsonSchema, root: JsonSchema = schema): unknown {
40
- return seedValue(schema, root, false);
41
+ return finiteSeed(seedValue(schema, root, false, new Set()));
41
42
  }
42
43
 
43
44
  /**
44
45
  * A SELF-CONSISTENT skeleton for a schema — the seed a variant switch (or any
45
46
  * other programmatic reseed) commits as the field's new value. Where
46
47
  * {@link defaultValueForSchema} deliberately leaves a required scalar absent so
47
- * a fresh form flags it, a committed skeleton must validate against its own
48
- * schema, so here EVERY required field with a known empty value is minted:
49
- * required strings seed `''`, numbers `0`, booleans `false` — consistently with
50
- * the `[]` / `{}` / nested-object seeds the default path already mints. Kinds
51
- * with no empty value to name (enum, union, free-form JSON) stay absent either
52
- * way: inventing a choice would be a lie the user never made.
48
+ * a fresh form flags it, a committed skeleton carries no missing required field,
49
+ * so here EVERY required field with a known empty value is minted: required
50
+ * strings seed `''`, numbers `0`, booleans `false`, a templated text an empty
51
+ * inline source `{ content: '' }` — consistently with the `[]` / `{}` /
52
+ * nested-object seeds the default path already mints. A minted empty value is
53
+ * not promised to pass its own value check: an empty templated text fails it
54
+ * ("needs inline content or a stored template"), as does `''` under a
55
+ * `minLength` or a `pattern`, and validation reports that error like any
56
+ * other. Kinds with no empty value to name (enum,
57
+ * union, free-form JSON) stay absent either way: inventing a choice would be a
58
+ * lie the user never made.
53
59
  */
54
60
  export function skeletonValueForSchema(schema: JsonSchema, root: JsonSchema = schema): unknown {
55
- return seedValue(schema, root, true);
61
+ return finiteSeed(seedValue(schema, root, true, new Set()));
62
+ }
63
+
64
+ /**
65
+ * The walk's answer when an object's required structure leads back into an object
66
+ * shape it is already inside with no `null` accepted anywhere on the way: that
67
+ * nesting never ends, so the object has no finite value.
68
+ */
69
+ const NO_FINITE_VALUE: unique symbol = Symbol('no finite value');
70
+
71
+ /**
72
+ * Thrown by {@link defaultValueForSchema} and {@link skeletonValueForSchema} when the
73
+ * schema admits no finite value: a chain of required properties refers back to its
74
+ * own definition with no `null` accepted on the way. A caller that seeds on a user
75
+ * action catches exactly this type to show the failure on its field; any other error
76
+ * is not a seeding outcome and propagates.
77
+ */
78
+ export class NoFiniteValueError extends Error {
79
+ constructor() {
80
+ super(
81
+ 'SchemaForm: the schema admits no finite value: its required properties refer back to their own definition with no null accepted on the way',
82
+ );
83
+ this.name = 'NoFiniteValueError';
84
+ }
85
+ }
86
+
87
+ /** The entry points' result: the seeded value, or {@link NoFiniteValueError} when none is finite. */
88
+ function finiteSeed(seed: unknown): unknown {
89
+ if (seed === NO_FINITE_VALUE) throw new NoFiniteValueError();
90
+ return seed;
56
91
  }
57
92
 
58
93
  /**
@@ -60,16 +95,44 @@ export function skeletonValueForSchema(schema: JsonSchema, root: JsonSchema = sc
60
95
  * carries an explicit default. A required property recurses; an optional one is
61
96
  * materialized only when it declares a `default`, so its own required children
62
97
  * never raise errors on a field the user did not opt into.
98
+ *
99
+ * `expanding` holds the object shapes being seeded on the way down. Reaching one of
100
+ * them again (a self-referencing definition) answers {@link NO_FINITE_VALUE}, and
101
+ * so does an object with any required property that answers it. A `nullable` object
102
+ * turns that answer into `null`, the value that ends the nesting there. So the
103
+ * answer climbs to the nearest nullable object above the point where the cycle
104
+ * closes, and reaches the entry point only when no object on the way accepts
105
+ * `null`: then no finite value exists.
63
106
  */
64
107
  function seedObject(
108
+ model: ModelOf<'object'>,
109
+ nullable: boolean,
110
+ root: JsonSchema,
111
+ mintScalars: boolean,
112
+ expanding: ReadonlySet<object>,
113
+ ): unknown {
114
+ const seeded = seedProperties(model, root, mintScalars, expanding);
115
+ return seeded === NO_FINITE_VALUE && nullable ? null : seeded;
116
+ }
117
+
118
+ /**
119
+ * An object's own seed (see {@link seedObject}): its properties' seeds, or
120
+ * {@link NO_FINITE_VALUE} when its shape is already being seeded above or a required
121
+ * property has no finite value.
122
+ */
123
+ function seedProperties(
65
124
  model: ModelOf<'object'>,
66
125
  root: JsonSchema,
67
126
  mintScalars: boolean,
68
- ): Record<string, unknown> {
127
+ expanding: ReadonlySet<object>,
128
+ ): unknown {
129
+ if (expanding.has(model.shape)) return NO_FINITE_VALUE;
130
+ const inside = new Set(expanding).add(model.shape);
69
131
  const entries: [string, unknown][] = [];
70
132
  for (const [name, propSchema] of model.properties) {
71
133
  if (model.required.has(name)) {
72
- const seeded = seedValue(propSchema, root, mintScalars);
134
+ const seeded = seedValue(propSchema, root, mintScalars, inside);
135
+ if (seeded === NO_FINITE_VALUE) return NO_FINITE_VALUE;
73
136
  if (seeded !== undefined) entries.push([name, seeded]);
74
137
  continue;
75
138
  }
@@ -101,9 +164,16 @@ function seedScalar(kind: 'string' | 'number' | 'boolean', mintScalars: boolean)
101
164
  /**
102
165
  * The shared seeding walk. `mintScalars` distinguishes the two entry points
103
166
  * above: `false` leaves value-less required scalars absent; `true` mints their
104
- * type's empty value.
167
+ * type's empty value. `expanding` holds the object shapes being seeded on the way
168
+ * down; a nullable object whose own seed has no finite value seeds `null` (see
169
+ * {@link seedObject}).
105
170
  */
106
- function seedValue(schema: JsonSchema, root: JsonSchema, mintScalars: boolean): unknown {
171
+ function seedValue(
172
+ schema: JsonSchema,
173
+ root: JsonSchema,
174
+ mintScalars: boolean,
175
+ expanding: ReadonlySet<object>,
176
+ ): unknown {
107
177
  const classified = classifySchema(schema, root);
108
178
  const explicit = explicitDefault(schema, classified.schema);
109
179
  if (explicit.present) return explicit.value;
@@ -113,7 +183,7 @@ function seedValue(schema: JsonSchema, root: JsonSchema, mintScalars: boolean):
113
183
  case 'const':
114
184
  return model.value;
115
185
  case 'object':
116
- return seedObject(model, root, mintScalars);
186
+ return seedObject(model, classified.nullable, root, mintScalars, expanding);
117
187
  case 'array':
118
188
  case 'multiselect':
119
189
  return [];
@@ -123,6 +193,10 @@ function seedValue(schema: JsonSchema, root: JsonSchema, mintScalars: boolean):
123
193
  case 'number':
124
194
  case 'boolean':
125
195
  return seedScalar(model.kind, mintScalars);
196
+ case 'templated-text':
197
+ // Inline is the templated text's own starting mode, so its empty value is an
198
+ // empty inline source; the default path leaves it absent like a scalar.
199
+ return mintScalars ? { content: '' } : undefined;
126
200
  case 'enum':
127
201
  case 'union':
128
202
  case 'json':
@@ -0,0 +1,162 @@
1
+ /**
2
+ * The injected expression door as a schema-form field: an `x-tai42-expression`
3
+ * annotation mapped onto the door's input-shape descriptor, and the door mounted
4
+ * inside a Suspense boundary that paints a resting shell while a lazily-injected
5
+ * door resolves. A jq-annotated string draws it as the whole field; a jq-annotated
6
+ * templated-text field draws it as its inline editor.
7
+ *
8
+ * NO JQ EDGE: the door arrives from the host (see `ExpressionFieldContext`);
9
+ * this module names no jq type.
10
+ */
11
+ import type { ReactNode } from 'react';
12
+ import { Suspense, useMemo } from 'react';
13
+
14
+ import { Field } from '../components/field';
15
+ import { Textarea } from '../components/inputs';
16
+ import type { ExpressionFieldComponent, ExpressionInputShape } from './context';
17
+ import type { ExpressionAnnotation } from './field-model';
18
+
19
+ /**
20
+ * An expression-annotated field, rendered through the injected door: a
21
+ * multiline resting control with the door's own visual-editor button. The door
22
+ * brings its own label/description/error chrome (a11y-linked slots), so it is NOT
23
+ * wrapped in the form's `Field` component — the annotation's descriptor fields map
24
+ * onto the input-shape descriptor, and the form's per-field error feeds `error`.
25
+ *
26
+ * The door is rendered as an ELEMENT, never called as a function, so its hooks and
27
+ * state (editor open, worker handle) belong to it and survive re-renders of this
28
+ * field.
29
+ *
30
+ * It mounts inside a `Suspense` boundary because a host is free to inject a `lazy`
31
+ * door to keep the jq subgraph in a split chunk of ITS OWN bundle. The fallback
32
+ * paints the resting shell — label/description/error chrome around the current
33
+ * value in a read-only multiline control — so the field holds its layout and keeps
34
+ * the value visible while that chunk resolves, and the swap does not flash. A door
35
+ * injected eagerly never suspends and renders straight through.
36
+ *
37
+ * The form passes only the props the field owns; a jq door's `serverValidate`,
38
+ * `onEditorOpenChange`, and `compact` stay unwired, because no author-time
39
+ * validation endpoint applies to a schema-declared field, the hosts that render
40
+ * `SchemaForm` register no global shortcuts to mute, and every sibling field shows
41
+ * a visible label (the compact variant is for dense host rows).
42
+ */
43
+ export function ExpressionField({
44
+ component: Door,
45
+ heading,
46
+ description,
47
+ error,
48
+ expression,
49
+ argName,
50
+ value,
51
+ onChange,
52
+ }: {
53
+ component: ExpressionFieldComponent;
54
+ heading: string;
55
+ description: string | undefined;
56
+ error: string | undefined;
57
+ expression: ExpressionAnnotation;
58
+ argName: string;
59
+ value: string;
60
+ onChange: (value: string) => void;
61
+ }): ReactNode {
62
+ // Memoised: the descriptor's identity feeds the door's own memoisation, so a
63
+ // fresh object each keystroke would defeat it.
64
+ const shape = useMemo(() => expressionShape(expression, argName), [expression, argName]);
65
+ return (
66
+ <Suspense
67
+ fallback={
68
+ <ExpressionFieldSkeleton
69
+ heading={heading}
70
+ description={description}
71
+ error={error}
72
+ value={value}
73
+ />
74
+ }
75
+ >
76
+ <Door
77
+ label={heading}
78
+ description={description}
79
+ error={error}
80
+ shape={shape}
81
+ multiline
82
+ value={value}
83
+ onChange={onChange}
84
+ />
85
+ </Suspense>
86
+ );
87
+ }
88
+
89
+ /**
90
+ * The resting shell shown while a lazily-injected door resolves. It mirrors the
91
+ * live door's footprint — the same label/description/error chrome wrapping a
92
+ * multiline control seeded with the current value — but the control is
93
+ * non-interactive (read-only, `aria-busy`) because there is nothing to edit yet. Matching the
94
+ * footprint is what keeps the Suspense swap from shifting layout or flashing.
95
+ */
96
+ function ExpressionFieldSkeleton({
97
+ heading,
98
+ description,
99
+ error,
100
+ value,
101
+ }: {
102
+ heading: string;
103
+ description: string | undefined;
104
+ error: string | undefined;
105
+ value: string;
106
+ }): ReactNode {
107
+ return (
108
+ <Field label={heading} description={description} error={error}>
109
+ <Textarea value={value} readOnly aria-busy="true" />
110
+ </Field>
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Map a classified expression annotation onto the door's input-shape descriptor.
116
+ * A bare annotation (`language` only) maps to NO descriptor — an undeclared shape
117
+ * is the honest rendering when the server said nothing about `.`. When any
118
+ * descriptor field is present, the required descriptor members the annotation
119
+ * omits fall back to neutral values rather than invented copy.
120
+ */
121
+ function expressionShape(
122
+ expression: ExpressionAnnotation,
123
+ argName: string,
124
+ ): ExpressionInputShape | undefined {
125
+ const declared =
126
+ [
127
+ expression.label,
128
+ expression.blurb,
129
+ expression.keys,
130
+ expression.variables,
131
+ expression.returns,
132
+ expression.caveats,
133
+ ].some((field) => field !== undefined) || expression.hasSample;
134
+ if (!declared) return undefined;
135
+ return {
136
+ // Opaque, host-namespaced, stable per field (memoisation/telemetry only).
137
+ id: argName === '' ? 'tai42.schema-form' : `tai42.schema-form.${argName}`,
138
+ label: expression.label ?? 'input',
139
+ blurb: expression.blurb ?? '',
140
+ keys: expression.keys ?? [],
141
+ returns: expression.returns ?? '',
142
+ ...(expression.caveats === undefined ? {} : { caveats: expression.caveats }),
143
+ ...(expression.hasSample ? { sample: expression.sample } : {}),
144
+ ...(expression.variables === undefined
145
+ ? {}
146
+ : { variables: expression.variables.map(expressionVariable) }),
147
+ };
148
+ }
149
+
150
+ /** Map one classified annotation variable onto the door's variable descriptor. */
151
+ function expressionVariable(
152
+ variable: NonNullable<ExpressionAnnotation['variables']>[number],
153
+ ): NonNullable<ExpressionInputShape['variables']>[number] {
154
+ return {
155
+ name: variable.name,
156
+ blurb: variable.blurb,
157
+ // The annotation glosses no sub-keys of a variable's value, so the door gets
158
+ // the variable's shape as untyped (empty keys).
159
+ keys: [],
160
+ ...(variable.hasSample ? { sample: variable.sample } : {}),
161
+ };
162
+ }
@@ -70,10 +70,11 @@ export interface ExpressionAnnotationVariable {
70
70
  }
71
71
 
72
72
  /**
73
- * The `x-tai42-expression` annotation on a string schema: the server declares
74
- * that the field's value is authored in a pipeline expression language (only jq
75
- * today) and optionally describes what `.` is for that expression. The renderer
76
- * uses it to open the visual-editor door instead of a plain text box. The shape
73
+ * The `x-tai42-expression` annotation on a string or templated-text property: the
74
+ * server declares that the field's value is authored in a pipeline expression
75
+ * language (only jq today) and optionally describes what `.` is for that expression.
76
+ * The renderer uses it to open the visual-editor door instead of a plain text box
77
+ * (on a templated-text field, as its inline editor). The shape
77
78
  * mirrors jq-studio's input-shape descriptor WITHOUT importing it — classification
78
79
  * stays a pure schema concern; the renderer maps this onto a jq field declaration.
79
80
  */
@@ -165,6 +166,13 @@ export type FieldModel =
165
166
  readonly kind: 'object';
166
167
  readonly properties: readonly (readonly [string, JsonSchema])[];
167
168
  readonly required: ReadonlySet<string>;
169
+ /**
170
+ * The schema's own `properties` map, by identity. Every classification of one
171
+ * definition — through a `$ref`, a null-union or single-`allOf` wrapper, or an
172
+ * annotated property site — carries this same map, so a walker recognises a
173
+ * definition nested inside itself (a self-referencing schema) by it.
174
+ */
175
+ readonly shape: Readonly<Record<string, JsonSchema>>;
168
176
  }
169
177
  | {
170
178
  // A string-keyed map typed by `additionalProperties` (no fixed
@@ -174,10 +182,25 @@ export type FieldModel =
174
182
  readonly kind: 'record';
175
183
  readonly values: JsonSchema;
176
184
  }
185
+ | {
186
+ // A property typed as the platform's templated text (its schema carries
187
+ // `x-tai42-templated-text`): inline `content` or a stored template `id`, plus
188
+ // the render `kwargs`. The value is that object, authored as one field.
189
+ readonly kind: 'templated-text';
190
+ /** Present when the property's `x-tai42-expression` annotation declares its
191
+ * inline content an expression: the inline editor is the expression door. */
192
+ readonly expression: ExpressionAnnotation | undefined;
193
+ }
177
194
  | {
178
195
  readonly kind: 'union';
179
196
  readonly variants: readonly UnionVariant[];
180
197
  readonly discriminator: string | undefined;
198
+ /**
199
+ * The schema's own `anyOf`/`oneOf` member list, by identity: every
200
+ * classification of one union carries this same list, so a field can tell
201
+ * whether it still draws the union it drew before.
202
+ */
203
+ readonly members: readonly JsonSchema[];
181
204
  }
182
205
  | {
183
206
  // A shape with no structured editor to build — a property-less object, a
@@ -4,14 +4,16 @@
4
4
  * inline; every other kind delegates to a dedicated field component). Object,
5
5
  * array, and union renderers recurse back through here for their children.
6
6
  */
7
- import { memo, type ReactNode } from 'react';
7
+ import { memo, type ReactNode, useContext } from 'react';
8
8
 
9
9
  import { Checkbox } from '../components/checkbox';
10
10
  import { Field } from '../components/field';
11
11
  import { NumberInput, TextInput } from '../components/inputs';
12
- import { ownValue } from '../guards';
12
+ import { isRecord, ownValue } from '../guards';
13
13
  import { ArrayField } from './array-field';
14
14
  import { classifySchema } from './classify';
15
+ import { CollapsedObjectField } from './collapsed-object-field';
16
+ import { ExpandedShapesContext } from './context';
15
17
  import { EnumField } from './enum-field';
16
18
  import { FieldGroup } from './field-group';
17
19
  import type { FieldModel } from './field-model';
@@ -21,6 +23,7 @@ import { ObjectFields } from './object-fields';
21
23
  import { RecordField } from './record-field';
22
24
  import { scalarLabel } from './resolve';
23
25
  import { StringField } from './string-field';
26
+ import { TemplatedField } from './templated-field';
24
27
  import type { FieldWords, JsonSchema, SchemaFormErrors } from './types';
25
28
  import { UnionField } from './union-field';
26
29
 
@@ -67,12 +70,15 @@ function labelText(explicit: string | undefined, label: string | undefined): str
67
70
  */
68
71
  function FieldNodeImpl(props: FieldNodeProps): ReactNode {
69
72
  const { root, value, onChange, path, required, idPrefix, errors } = props;
73
+ const expanded = useContext(ExpandedShapesContext);
70
74
  const classified = classifySchema(props.schema, root);
71
75
  const base = labelText(classified.title, props.label);
72
76
  const heading = props.markRequired === true && required ? `${base} *` : base;
73
77
  const { model, description } = classified;
74
78
  return renderField({
75
79
  model,
80
+ definition: classified.schema,
81
+ expanded,
76
82
  nullable: classified.nullable,
77
83
  heading,
78
84
  description,
@@ -93,6 +99,10 @@ function FieldNodeImpl(props: FieldNodeProps): ReactNode {
93
99
  /** The props the per-kind renderer needs, computed once by {@link FieldNodeImpl}. */
94
100
  interface RenderFieldArgs {
95
101
  readonly model: FieldModel;
102
+ /** The ref-resolved schema the model was classified from. */
103
+ readonly definition: JsonSchema;
104
+ /** The object shapes the fields above this one are drawing. */
105
+ readonly expanded: ReadonlySet<object>;
96
106
  readonly nullable: boolean;
97
107
  readonly heading: string;
98
108
  readonly description: string | undefined;
@@ -187,6 +197,19 @@ function renderField(args: RenderFieldArgs): ReactNode {
187
197
  onChange={onChange}
188
198
  />
189
199
  );
200
+ case 'templated-text':
201
+ return (
202
+ <TemplatedField
203
+ heading={heading}
204
+ description={description}
205
+ error={error}
206
+ expression={model.expression}
207
+ argName={path}
208
+ required={required}
209
+ value={value}
210
+ onChange={onChange}
211
+ />
212
+ );
190
213
  case 'multiselect':
191
214
  return (
192
215
  <MultiSelectField
@@ -228,10 +251,26 @@ function renderCompositeField(
228
251
  />
229
252
  );
230
253
  case 'object':
254
+ // A definition nested inside itself draws its fields only for a level the
255
+ // value holds; an absent level is one Add button, so the nesting stops at the data.
256
+ if (args.expanded.has(model.shape) && !isRecord(value)) {
257
+ return (
258
+ <CollapsedObjectField
259
+ heading={heading}
260
+ description={description}
261
+ error={error}
262
+ definition={args.definition}
263
+ shape={model.shape}
264
+ root={root}
265
+ onChange={onChange}
266
+ />
267
+ );
268
+ }
231
269
  return (
232
270
  <FieldGroup heading={heading} description={description} error={error} atRoot={path === ''}>
233
271
  <ObjectFields
234
272
  properties={model.properties}
273
+ shape={model.shape}
235
274
  requiredKeys={model.required}
236
275
  root={root}
237
276
  value={value}
@@ -35,12 +35,13 @@ export type {
35
35
  ExpressionFieldProps,
36
36
  ExpressionInputKey,
37
37
  ExpressionInputShape,
38
+ UseTemplatedTextCatalog,
38
39
  } from './context';
39
40
  export { RecordEntryRendererContext } from './context';
40
- export { ExpressionFieldContext } from './context';
41
+ export { ExpressionFieldContext, TemplatedTextCatalogContext } from './context';
41
42
  export type { DateRangePair } from './date-range-field';
42
43
  export { dateRangePairs, fieldDrawingOrder } from './date-range-field';
43
- export { defaultValueForSchema } from './default-value';
44
+ export { defaultValueForSchema, NoFiniteValueError } from './default-value';
44
45
  export { resolveRef } from './resolve';
45
46
  export type { CompletionProvider, SchemaFormProps } from './SchemaForm';
46
47
  export { SchemaForm } from './SchemaForm';
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * Renders the property fields of an object schema, one {@link FieldNode} per
3
- * property. Each child's edits are folded back into an immutable copy of the
3
+ * property, and records the object's `shape` as expanded for every field below it
4
+ * (see {@link ExpandedShapesContext}). Each child's edits are folded back into an immutable copy of the
4
5
  * object value; an optional `skip` omits a property (used for a union's
5
6
  * discriminator tag, which the variant picker owns).
6
7
  */
7
- import type { ReactNode } from 'react';
8
+ import { type ReactNode, useContext, useMemo } from 'react';
8
9
 
9
10
  import { ownValue } from '../guards';
11
+ import { ExpandedShapesContext } from './context';
10
12
  import { DateRangeField, dateRangePairs, fieldDrawingOrder } from './date-range-field';
11
13
  import { FieldNode } from './field-node';
12
14
  import { useKeyedCallbacks } from './keyed-callbacks';
@@ -19,6 +21,7 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
19
21
 
20
22
  export function ObjectFields({
21
23
  properties,
24
+ shape,
22
25
  requiredKeys,
23
26
  root,
24
27
  value,
@@ -31,6 +34,8 @@ export function ObjectFields({
31
34
  fieldWording,
32
35
  }: {
33
36
  properties: readonly (readonly [string, JsonSchema])[];
37
+ /** The object model's `shape`, recorded as expanded for the fields below. */
38
+ shape: object;
34
39
  requiredKeys: ReadonlySet<string>;
35
40
  root: JsonSchema;
36
41
  value: unknown;
@@ -50,6 +55,8 @@ export function ObjectFields({
50
55
  fieldWording?: Readonly<Record<string, FieldWords>>;
51
56
  }): ReactNode {
52
57
  const obj = isPlainObject(value) ? value : {};
58
+ const above = useContext(ExpandedShapesContext);
59
+ const expanded = useMemo(() => new Set(above).add(shape), [above, shape]);
53
60
  const changeFor = useKeyedCallbacks<string, unknown>(
54
61
  (name, next) => {
55
62
  onChange(setKey(obj, name, next));
@@ -96,7 +103,7 @@ export function ObjectFields({
96
103
  const timeOf = (name: string | undefined, propSchema: JsonSchema | undefined): ReactNode =>
97
104
  name === undefined || propSchema === undefined ? null : fieldOf(name, propSchema);
98
105
  return (
99
- <>
106
+ <ExpandedShapesContext.Provider value={expanded}>
100
107
  {drawn.map((name) => {
101
108
  const propSchema = schemaByName.get(name);
102
109
  if (propSchema === undefined) return null;
@@ -127,7 +134,7 @@ export function ObjectFields({
127
134
  }
128
135
  return fieldOf(name, propSchema);
129
136
  })}
130
- </>
137
+ </ExpandedShapesContext.Provider>
131
138
  );
132
139
  }
133
140
 
@@ -23,6 +23,7 @@ import { defaultValueForSchema } from './default-value';
23
23
  import { FieldGroup } from './field-group';
24
24
  import { FieldNode } from './field-node';
25
25
  import { useKeyedCallbacks } from './keyed-callbacks';
26
+ import { useSeedingAction } from './seeding-action';
26
27
  import type { JsonSchema, SchemaFormErrors } from './types';
27
28
 
28
29
  interface Row {
@@ -126,8 +127,10 @@ export function RecordField({
126
127
  setValue,
127
128
  rows.map((row) => row.id),
128
129
  );
130
+ const add = useSeedingAction(values);
129
131
  const addRow = (): void => {
130
- commit([...rows, { id: nextId.current++, key: '', value: newEntryValue(values, root) }]);
132
+ const entry = add.seed(() => newEntryValue(values, root));
133
+ if (entry.ok) commit([...rows, { id: nextId.current++, key: '', value: entry.value }]);
131
134
  };
132
135
  const removeRow = (id: number): void => {
133
136
  commit(rows.filter((row) => row.id !== id));
@@ -137,7 +140,12 @@ export function RecordField({
137
140
  const summary = keyFaultSummary(blankRowIds.size, duplicateRowIds.size);
138
141
 
139
142
  return (
140
- <FieldGroup heading={heading} description={description} error={error} atRoot={false}>
143
+ <FieldGroup
144
+ heading={heading}
145
+ description={description}
146
+ error={add.failure ?? error}
147
+ atRoot={false}
148
+ >
141
149
  {rows.length === 0 ? <span className="tai-field-hint">No entries</span> : null}
142
150
  {summary !== undefined ? (
143
151
  <span role="alert" className="tai-field-error">
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The state behind a control that seeds a fresh value on a user action: a list's
3
+ * "Add item", a map's "Add entry", a nested level's "Add", and the union variant
4
+ * picker's switch. Seeding throws {@link NoFiniteValueError} when the schema admits no
5
+ * finite value, and a throw inside an event handler never reaches an error boundary,
6
+ * so that failure is logged and kept as the field's error message. Only the seed runs
7
+ * inside the catch, and only that error type is caught: the caller commits the value
8
+ * (its `onChange`) outside it, so every other error propagates as thrown.
9
+ *
10
+ * A failure belongs to the `subject` it was raised for (the schema identity the
11
+ * caller seeds from): when the same field later draws a different subject, the
12
+ * failure is no longer reported. The next successful seed clears it.
13
+ */
14
+ import { useState } from 'react';
15
+
16
+ import { NoFiniteValueError } from './default-value';
17
+
18
+ /** A seed attempt's outcome: the seeded value, or a failure now shown on the field. */
19
+ export type SeedOutcome = { readonly ok: true; readonly value: unknown } | { readonly ok: false };
20
+
21
+ export interface SeedingAction {
22
+ /** The failure message for the current subject, or `undefined` when there is none. */
23
+ readonly failure: string | undefined;
24
+ /** Run `make`, the seeding step; a {@link NoFiniteValueError} becomes the failure. */
25
+ readonly seed: (make: () => unknown) => SeedOutcome;
26
+ }
27
+
28
+ export function useSeedingAction(subject: object): SeedingAction {
29
+ const [failed, setFailed] = useState<{ subject: object; message: string } | undefined>(undefined);
30
+ const seed = (make: () => unknown): SeedOutcome => {
31
+ let value: unknown;
32
+ try {
33
+ value = make();
34
+ } catch (error) {
35
+ if (!(error instanceof NoFiniteValueError)) throw error;
36
+ console.error('SchemaForm: seeding a new value failed', error);
37
+ setFailed({ subject, message: error.message });
38
+ return { ok: false };
39
+ }
40
+ setFailed(undefined);
41
+ return { ok: true, value };
42
+ };
43
+ return { failure: failed?.subject === subject ? failed.message : undefined, seed };
44
+ }