@tai42/studio-sdk 20.0.0 → 22.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 (143) hide show
  1. package/dist/components/chip-group.d.ts +42 -0
  2. package/dist/components/chip-group.d.ts.map +1 -0
  3. package/dist/components/chip-group.js +85 -0
  4. package/dist/components/chip-group.js.map +1 -0
  5. package/dist/components/components.css +16 -3
  6. package/dist/components/execution-key-picker.js.map +1 -1
  7. package/dist/components/extension-grouping.d.ts +8 -12
  8. package/dist/components/extension-grouping.d.ts.map +1 -1
  9. package/dist/components/extension-grouping.js +12 -15
  10. package/dist/components/extension-grouping.js.map +1 -1
  11. package/dist/components/extension-picker.d.ts +3 -4
  12. package/dist/components/extension-picker.d.ts.map +1 -1
  13. package/dist/components/extension-picker.js +10 -10
  14. package/dist/components/extension-picker.js.map +1 -1
  15. package/dist/components/fleet-report.d.ts +10 -5
  16. package/dist/components/fleet-report.d.ts.map +1 -1
  17. package/dist/components/fleet-report.js +20 -3
  18. package/dist/components/fleet-report.js.map +1 -1
  19. package/dist/components/json-tree-model.d.ts +19 -7
  20. package/dist/components/json-tree-model.d.ts.map +1 -1
  21. package/dist/components/json-tree-model.js +32 -8
  22. package/dist/components/json-tree-model.js.map +1 -1
  23. package/dist/components/json-tree.d.ts +10 -1
  24. package/dist/components/json-tree.d.ts.map +1 -1
  25. package/dist/components/json-tree.js +7 -4
  26. package/dist/components/json-tree.js.map +1 -1
  27. package/dist/components/select.d.ts +6 -0
  28. package/dist/components/select.d.ts.map +1 -1
  29. package/dist/components/select.js +3 -2
  30. package/dist/components/select.js.map +1 -1
  31. package/dist/components/served-media.d.ts +50 -4
  32. package/dist/components/served-media.d.ts.map +1 -1
  33. package/dist/components/served-media.js +69 -19
  34. package/dist/components/served-media.js.map +1 -1
  35. package/dist/env-markers.d.ts +25 -0
  36. package/dist/env-markers.d.ts.map +1 -0
  37. package/dist/env-markers.js +63 -0
  38. package/dist/env-markers.js.map +1 -0
  39. package/dist/hooks/interactions-stream.d.ts.map +1 -1
  40. package/dist/hooks/interactions-stream.js +9 -7
  41. package/dist/hooks/interactions-stream.js.map +1 -1
  42. package/dist/hooks/useApi.d.ts +2264 -912
  43. package/dist/hooks/useApi.d.ts.map +1 -1
  44. package/dist/hooks/useCapabilities.d.ts +5 -9
  45. package/dist/hooks/useCapabilities.d.ts.map +1 -1
  46. package/dist/hooks/useCapabilities.js +8 -33
  47. package/dist/hooks/useCapabilities.js.map +1 -1
  48. package/dist/host.d.ts +2 -0
  49. package/dist/host.d.ts.map +1 -1
  50. package/dist/host.js +8 -0
  51. package/dist/host.js.map +1 -1
  52. package/dist/index.d.ts +12 -6
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +5 -3
  55. package/dist/index.js.map +1 -1
  56. package/dist/plugin/coverage.d.ts +14 -0
  57. package/dist/plugin/coverage.d.ts.map +1 -0
  58. package/dist/plugin/coverage.js +14 -0
  59. package/dist/plugin/coverage.js.map +1 -0
  60. package/dist/plugin/registry.d.ts +11 -1
  61. package/dist/plugin/registry.d.ts.map +1 -1
  62. package/dist/plugin/registry.js +33 -4
  63. package/dist/plugin/registry.js.map +1 -1
  64. package/dist/plugin/types.d.ts +43 -1
  65. package/dist/plugin/types.d.ts.map +1 -1
  66. package/dist/schema-form/SchemaForm.d.ts +11 -1
  67. package/dist/schema-form/SchemaForm.d.ts.map +1 -1
  68. package/dist/schema-form/SchemaForm.js +2 -2
  69. package/dist/schema-form/SchemaForm.js.map +1 -1
  70. package/dist/schema-form/classify.d.ts.map +1 -1
  71. package/dist/schema-form/classify.js +54 -2
  72. package/dist/schema-form/classify.js.map +1 -1
  73. package/dist/schema-form/date-range-field.d.ts +44 -0
  74. package/dist/schema-form/date-range-field.d.ts.map +1 -0
  75. package/dist/schema-form/date-range-field.js +72 -0
  76. package/dist/schema-form/date-range-field.js.map +1 -0
  77. package/dist/schema-form/enum-field.d.ts +3 -1
  78. package/dist/schema-form/enum-field.d.ts.map +1 -1
  79. package/dist/schema-form/enum-field.js +8 -2
  80. package/dist/schema-form/enum-field.js.map +1 -1
  81. package/dist/schema-form/field-model.d.ts +20 -1
  82. package/dist/schema-form/field-model.d.ts.map +1 -1
  83. package/dist/schema-form/field-node.d.ts +10 -0
  84. package/dist/schema-form/field-node.d.ts.map +1 -1
  85. package/dist/schema-form/field-node.js +6 -3
  86. package/dist/schema-form/field-node.js.map +1 -1
  87. package/dist/schema-form/index.d.ts +3 -1
  88. package/dist/schema-form/index.d.ts.map +1 -1
  89. package/dist/schema-form/index.js +1 -0
  90. package/dist/schema-form/index.js.map +1 -1
  91. package/dist/schema-form/multiselect-field.d.ts.map +1 -1
  92. package/dist/schema-form/multiselect-field.js +11 -0
  93. package/dist/schema-form/multiselect-field.js.map +1 -1
  94. package/dist/schema-form/object-fields.d.ts +9 -1
  95. package/dist/schema-form/object-fields.d.ts.map +1 -1
  96. package/dist/schema-form/object-fields.js +16 -3
  97. package/dist/schema-form/object-fields.js.map +1 -1
  98. package/dist/schema-form/string-field.d.ts +6 -1
  99. package/dist/schema-form/string-field.d.ts.map +1 -1
  100. package/dist/schema-form/string-field.js +42 -8
  101. package/dist/schema-form/string-field.js.map +1 -1
  102. package/dist/schema-form/types.d.ts +36 -0
  103. package/dist/schema-form/types.d.ts.map +1 -1
  104. package/dist/state-binding/UpdateRow.d.ts +2 -2
  105. package/dist/state-binding/UpdateRow.js +1 -1
  106. package/dist/state-binding/UpdateRow.js.map +1 -1
  107. package/dist/state-binding/adapter.d.ts +12 -8
  108. package/dist/state-binding/adapter.d.ts.map +1 -1
  109. package/dist/state-binding/adapter.js +30 -13
  110. package/dist/state-binding/adapter.js.map +1 -1
  111. package/package.json +5 -4
  112. package/src/components/chip-group.tsx +157 -0
  113. package/src/components/components.css +16 -3
  114. package/src/components/execution-key-picker.tsx +1 -1
  115. package/src/components/extension-grouping.ts +22 -22
  116. package/src/components/extension-picker.tsx +13 -14
  117. package/src/components/fleet-report.tsx +66 -7
  118. package/src/components/json-tree-model.ts +36 -8
  119. package/src/components/json-tree.tsx +24 -4
  120. package/src/components/select.tsx +11 -1
  121. package/src/components/served-media.tsx +150 -16
  122. package/src/env-markers.ts +70 -0
  123. package/src/hooks/interactions-stream.ts +9 -7
  124. package/src/hooks/useCapabilities.ts +8 -31
  125. package/src/host.ts +2 -0
  126. package/src/index.ts +22 -2
  127. package/src/plugin/coverage.ts +23 -0
  128. package/src/plugin/registry.ts +58 -3
  129. package/src/plugin/types.ts +45 -1
  130. package/src/schema-form/SchemaForm.tsx +10 -0
  131. package/src/schema-form/classify.ts +59 -3
  132. package/src/schema-form/date-range-field.tsx +167 -0
  133. package/src/schema-form/enum-field.tsx +20 -0
  134. package/src/schema-form/field-model.ts +24 -2
  135. package/src/schema-form/field-node.tsx +19 -0
  136. package/src/schema-form/index.ts +10 -1
  137. package/src/schema-form/multiselect-field.tsx +21 -0
  138. package/src/schema-form/object-fields.tsx +36 -2
  139. package/src/schema-form/string-field.tsx +72 -1
  140. package/src/schema-form/types.ts +38 -0
  141. package/src/state-binding/README.md +10 -7
  142. package/src/state-binding/UpdateRow.tsx +3 -3
  143. package/src/state-binding/adapter.ts +31 -16
@@ -5,7 +5,7 @@
5
5
  * validator import the vocabulary without pulling in classification code.
6
6
  */
7
7
  import type { EnumOption } from './resolve';
8
- import type { JsonSchema } from './types';
8
+ import type { FormControl, FormInputMode, JsonSchema } from './types';
9
9
 
10
10
  /**
11
11
  * How a string field accepts binary content as a JSON-serializable string.
@@ -115,10 +115,28 @@ export interface UnionVariant {
115
115
  /** The renderer-facing classification of a schema node. */
116
116
  export type FieldModel =
117
117
  | { readonly kind: 'const'; readonly value: unknown }
118
- | { readonly kind: 'enum'; readonly options: readonly EnumOption[] }
118
+ | {
119
+ readonly kind: 'enum';
120
+ readonly options: readonly EnumOption[];
121
+ /** The `control` keyword off the property: `'chips'` draws the choice as a
122
+ * chip grid in place of the radio/select; `undefined` keeps the default. */
123
+ readonly control: FormControl | undefined;
124
+ }
119
125
  | {
120
126
  readonly kind: 'string';
121
127
  readonly format: string | undefined;
128
+ /** The input kind (keyboard / native control) this string draws, from the
129
+ * schema's `inputMode` keyword; `undefined` for a plain text box. It
130
+ * draws, it does not validate, and it outranks `format` for the control. */
131
+ readonly inputMode: FormInputMode | undefined;
132
+ /** True when the `control` keyword is `'textarea'`: draw a multi-line text
133
+ * control instead of a single-line box. It draws, it does not validate. */
134
+ readonly multiline: boolean;
135
+ /** The `maxLength`/`minLength` JSON-Schema bounds, drawn on the control (a
136
+ * native cap plus a live counter when `maxLength` is set) and enforced by
137
+ * the validator. `undefined` leaves that bound unconstrained. */
138
+ readonly maxLength: number | undefined;
139
+ readonly minLength: number | undefined;
122
140
  /** Present when media annotations opt the field into the upload control. */
123
141
  readonly media: MediaUpload | undefined;
124
142
  /** Present when a well-formed `x-tai42-expression` annotation opts the
@@ -138,6 +156,10 @@ export type FieldModel =
138
156
  // `array` field — a multi-select needs a known option set to draw.
139
157
  readonly kind: 'multiselect';
140
158
  readonly options: readonly EnumOption[];
159
+ /** The `control` keyword off the property: `'chips'` draws the multi-select
160
+ * as a toggle-chip grid in place of the checkbox group; `undefined` keeps
161
+ * the default. */
162
+ readonly control: FormControl | undefined;
141
163
  }
142
164
  | {
143
165
  readonly kind: 'object';
@@ -41,6 +41,14 @@ interface FieldNodeProps {
41
41
  readonly markRequired?: boolean;
42
42
  readonly errors: SchemaFormErrors | undefined;
43
43
  readonly idPrefix: string;
44
+ /**
45
+ * Per-field picker placeholders keyed by property name, handed to an OBJECT node so
46
+ * its direct `enum`-select children draw their placeholder. Carried by the root form
47
+ * only; nested objects receive none.
48
+ */
49
+ readonly fieldWording?: Readonly<Record<string, { choiceLabel?: string }>>;
50
+ /** This field's own picker placeholder, when it draws as an `enum` select. */
51
+ readonly choiceLabel?: string;
44
52
  }
45
53
 
46
54
  function labelText(explicit: string | undefined, label: string | undefined): string {
@@ -73,6 +81,8 @@ function FieldNodeImpl(props: FieldNodeProps): ReactNode {
73
81
  required,
74
82
  errors,
75
83
  idPrefix,
84
+ fieldWording: props.fieldWording,
85
+ choiceLabel: props.choiceLabel,
76
86
  });
77
87
  }
78
88
 
@@ -90,6 +100,8 @@ interface RenderFieldArgs {
90
100
  readonly required: boolean;
91
101
  readonly errors: SchemaFormErrors | undefined;
92
102
  readonly idPrefix: string;
103
+ readonly fieldWording?: Readonly<Record<string, { choiceLabel?: string }>>;
104
+ readonly choiceLabel?: string;
93
105
  }
94
106
 
95
107
  /** Render the field component matching the classified model kind. */
@@ -126,6 +138,7 @@ function renderField(args: RenderFieldArgs): ReactNode {
126
138
  model={model}
127
139
  value={value}
128
140
  onChange={onChange}
141
+ choiceLabel={args.choiceLabel}
129
142
  />
130
143
  );
131
144
  case 'string':
@@ -135,6 +148,10 @@ function renderField(args: RenderFieldArgs): ReactNode {
135
148
  description={description}
136
149
  error={error}
137
150
  format={model.format}
151
+ inputMode={model.inputMode}
152
+ multiline={model.multiline}
153
+ maxLength={model.maxLength}
154
+ minLength={model.minLength}
138
155
  media={model.media}
139
156
  expression={model.expression}
140
157
  date={model.date}
@@ -187,6 +204,7 @@ function renderCompositeField(
187
204
  model: Extract<FieldModel, { kind: 'array' | 'object' | 'record' | 'union' }>,
188
205
  ): ReactNode {
189
206
  const { heading, description, error, root, value, onChange, path, errors, idPrefix } = args;
207
+ const { fieldWording } = args;
190
208
  switch (model.kind) {
191
209
  case 'array':
192
210
  return (
@@ -216,6 +234,7 @@ function renderCompositeField(
216
234
  errors={errors}
217
235
  idPrefix={idPrefix}
218
236
  skip={undefined}
237
+ fieldWording={fieldWording}
219
238
  />
220
239
  </FieldGroup>
221
240
  );
@@ -38,12 +38,21 @@ export type {
38
38
  } from './context';
39
39
  export { RecordEntryRendererContext } from './context';
40
40
  export { ExpressionFieldContext } from './context';
41
+ export type { DateRangePair } from './date-range-field';
42
+ export { dateRangePairs } from './date-range-field';
41
43
  export { defaultValueForSchema } from './default-value';
42
44
  export { resolveRef } from './resolve';
43
45
  export type { CompletionProvider, SchemaFormProps } from './SchemaForm';
44
46
  export { SchemaForm } from './SchemaForm';
45
47
  export type { SecretRef, SecretRefFieldProps } from './SecretRefField';
46
48
  export { SecretRefField } from './SecretRefField';
47
- export type { Discriminator, JsonSchema, JsonSchemaType, SchemaFormErrors } from './types';
49
+ export type {
50
+ Discriminator,
51
+ FormControl,
52
+ FormInputMode,
53
+ JsonSchema,
54
+ JsonSchemaType,
55
+ SchemaFormErrors,
56
+ } from './types';
48
57
  export { validateAgainstSchema } from './validate';
49
58
  export { isFieldVisible } from './visibility';
@@ -8,6 +8,7 @@
8
8
  import type { ReactNode } from 'react';
9
9
 
10
10
  import { Checkbox } from '../components/checkbox';
11
+ import { ChipGroup } from '../components/chip-group';
11
12
  import { Field } from '../components/field';
12
13
  import type { FieldModel } from './field-model';
13
14
 
@@ -38,6 +39,26 @@ export function MultiSelectField({
38
39
  .filter((candidate) => next.includes(candidate));
39
40
  onChange(ordered);
40
41
  };
42
+
43
+ // `control: chips` draws the multi-select as a toggle-chip grid in place of the
44
+ // checkbox group. The chips emit the selection in the OPTIONS' declared order.
45
+ if (model.control === 'chips') {
46
+ return (
47
+ <Field label={heading} description={description} error={error} group>
48
+ <ChipGroup
49
+ mode="multi"
50
+ options={model.options.map((option) => ({
51
+ value: String(option.value),
52
+ label: option.label,
53
+ }))}
54
+ value={selected}
55
+ onValueChange={(next) => {
56
+ onChange([...next]);
57
+ }}
58
+ />
59
+ </Field>
60
+ );
61
+ }
41
62
  return (
42
63
  <Field label={heading} description={description} error={error} group>
43
64
  {model.options.map((option, index) => {
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { ReactNode } from 'react';
8
8
 
9
+ import { DateRangeField, dateRangePairs } from './date-range-field';
9
10
  import { FieldNode } from './field-node';
10
11
  import { useKeyedCallbacks } from './keyed-callbacks';
11
12
  import type { JsonSchema, SchemaFormErrors } from './types';
@@ -26,6 +27,7 @@ export function ObjectFields({
26
27
  idPrefix,
27
28
  skip,
28
29
  markRequired = false,
30
+ fieldWording,
29
31
  }: {
30
32
  properties: readonly (readonly [string, JsonSchema])[];
31
33
  requiredKeys: ReadonlySet<string>;
@@ -38,6 +40,12 @@ export function ObjectFields({
38
40
  skip: string | undefined;
39
41
  /** Mark each required property's label with the DS required marker. */
40
42
  markRequired?: boolean;
43
+ /**
44
+ * Per-property picker placeholders, keyed by property name. Handed to the root form's
45
+ * object only; a property whose `enum` draws as a select shows its entry's
46
+ * `choiceLabel` as the select placeholder. Absent for nested objects.
47
+ */
48
+ fieldWording?: Readonly<Record<string, { choiceLabel?: string }>>;
41
49
  }): ReactNode {
42
50
  const obj = isPlainObject(value) ? value : {};
43
51
  const changeFor = useKeyedCallbacks<string, unknown>(
@@ -46,14 +54,39 @@ export function ObjectFields({
46
54
  },
47
55
  properties.map(([name]) => name),
48
56
  );
57
+ // A drawable date-range pair renders as ONE DateRangeField at the START's slot; the
58
+ // END is skipped. The pairing is `dateRangePairs`, the one definition every surface
59
+ // shares. Properties here are one page's, so the "same page" rule holds by construction.
60
+ const pairs = dateRangePairs(properties);
61
+ const pairByStart = new Map(pairs.map((pair) => [pair.startName, pair]));
62
+ const skippedEnds = new Set(pairs.map((pair) => pair.endName));
63
+ const childPathOf = (name: string): string => (path === '' ? name : `${path}.${name}`);
49
64
  return (
50
65
  <>
51
66
  {properties.map(([name, propSchema]) => {
52
67
  if (skip !== undefined && name === skip) return null;
68
+ if (skippedEnds.has(name)) return null;
53
69
  // A field hidden by its `visibleWhen` predicate (scope B) is not rendered; the
54
70
  // validator skips it in step, so a hidden field is neither shown nor required.
55
71
  if (!isFieldVisible(propSchema, obj)) return null;
56
- const childPath = path === '' ? name : `${path}.${name}`;
72
+ const pair = pairByStart.get(name);
73
+ if (pair !== undefined) {
74
+ return (
75
+ <DateRangeField
76
+ key={name}
77
+ pair={pair}
78
+ startValue={obj[pair.startName]}
79
+ endValue={obj[pair.endName]}
80
+ startRequired={requiredKeys.has(pair.startName)}
81
+ endRequired={requiredKeys.has(pair.endName)}
82
+ markRequired={markRequired}
83
+ startError={errors?.[childPathOf(pair.startName)]}
84
+ endError={errors?.[childPathOf(pair.endName)]}
85
+ onChangeStart={changeFor(pair.startName)}
86
+ onChangeEnd={changeFor(pair.endName)}
87
+ />
88
+ );
89
+ }
57
90
  return (
58
91
  <FieldNode
59
92
  key={name}
@@ -61,12 +94,13 @@ export function ObjectFields({
61
94
  root={root}
62
95
  value={obj[name]}
63
96
  onChange={changeFor(name)}
64
- path={childPath}
97
+ path={childPathOf(name)}
65
98
  label={name}
66
99
  required={requiredKeys.has(name)}
67
100
  markRequired={markRequired}
68
101
  errors={errors}
69
102
  idPrefix={idPrefix}
103
+ choiceLabel={fieldWording?.[name]?.choiceLabel}
70
104
  />
71
105
  );
72
106
  })}
@@ -31,12 +31,17 @@ import { CompletionProviderContext, ExpressionFieldContext } from './context';
31
31
  import type { DateConstraints, ExpressionAnnotation, MediaUpload } from './field-model';
32
32
  import { MediaField } from './media-field';
33
33
  import type { CompletionProvider } from './SchemaForm';
34
+ import type { FormInputMode } from './types';
34
35
 
35
36
  export function StringField({
36
37
  heading,
37
38
  description,
38
39
  error,
39
40
  format,
41
+ inputMode,
42
+ multiline,
43
+ maxLength,
44
+ minLength,
40
45
  media,
41
46
  expression,
42
47
  date,
@@ -49,6 +54,10 @@ export function StringField({
49
54
  description: string | undefined;
50
55
  error: string | undefined;
51
56
  format: string | undefined;
57
+ inputMode: FormInputMode | undefined;
58
+ multiline: boolean;
59
+ maxLength: number | undefined;
60
+ minLength: number | undefined;
52
61
  media: MediaUpload | undefined;
53
62
  expression: ExpressionAnnotation | undefined;
54
63
  date: DateConstraints | undefined;
@@ -57,6 +66,7 @@ export function StringField({
57
66
  required: boolean;
58
67
  onChange: (value: unknown) => void;
59
68
  }): ReactNode {
69
+ const control = stringControl(format, inputMode);
60
70
  const completionProvider = useContext(CompletionProviderContext);
61
71
  const expressionField = useContext(ExpressionFieldContext);
62
72
  const current = typeof value === 'string' ? value : '';
@@ -114,24 +124,58 @@ export function StringField({
114
124
  onChange={emit}
115
125
  provider={completionProvider}
116
126
  />
127
+ ) : multiline ? (
128
+ // `control: textarea` draws a multi-line control; the length bounds draw
129
+ // natively, and a live counter shows the maximum (below).
130
+ <Textarea
131
+ maxLength={maxLength}
132
+ minLength={minLength}
133
+ value={current}
134
+ onChange={(event) => {
135
+ emit(event.target.value);
136
+ }}
137
+ />
117
138
  ) : (
118
139
  <TextInput
119
- type={stringInputType(format)}
140
+ type={control.type}
141
+ // The keyboard hint for a `numeric`/`decimal` mode (a text box with a
142
+ // numeric/decimal keyboard); `undefined` for every other control, which
143
+ // selects its kind through `type` instead.
144
+ inputMode={control.inputMode}
120
145
  // A native date control enforces its inclusive bounds (scope C); on a
121
146
  // non-date input the browser ignores these, and the validator enforces the
122
147
  // rest (unavailable days, range span) that the control cannot draw.
123
148
  min={date?.min}
124
149
  max={date?.max}
150
+ // The length bounds draw natively (the control caps input at `maxLength`)
151
+ // and the validator enforces them; a counter shows the maximum (below).
152
+ maxLength={maxLength}
153
+ minLength={minLength}
125
154
  value={current}
126
155
  onChange={(event) => {
127
156
  emit(event.target.value);
128
157
  }}
129
158
  />
130
159
  )}
160
+ {/* The live length counter, shown whenever a maximum is declared — on both the
161
+ text box and the text area. It counts code points, matching the validator's
162
+ own `maxLength` check. */}
163
+ {completionProvider === undefined && maxLength !== undefined ? (
164
+ <LengthCounter current={current} max={maxLength} />
165
+ ) : null}
131
166
  </Field>
132
167
  );
133
168
  }
134
169
 
170
+ /** The `n / max` length hint under a bounded string control. */
171
+ function LengthCounter({ current, max }: { current: string; max: number }): ReactNode {
172
+ return (
173
+ <p className="tai-field-hint" style={{ margin: 0 }} aria-live="polite">
174
+ {`${String(Array.from(current).length)} / ${String(max)}`}
175
+ </p>
176
+ );
177
+ }
178
+
135
179
  /**
136
180
  * A string field backed by argument completions. Split out so its memoised
137
181
  * hooks stay unconditional (they cannot live behind the provider check in
@@ -360,3 +404,30 @@ function stringInputType(format: string | undefined): string {
360
404
  if (format === undefined) return 'text';
361
405
  return FORMAT_INPUT_TYPES[format] ?? 'text';
362
406
  }
407
+
408
+ /** The native control each `inputMode` draws. `tel`/`email`/`url` select the
409
+ * matching input `type`; `numeric`/`decimal` keep a text box and set the HTML
410
+ * `inputmode` keyboard hint; `text` is a plain text box. */
411
+ const INPUT_MODE_CONTROLS: Record<
412
+ FormInputMode,
413
+ { readonly type: string; readonly inputMode: 'numeric' | 'decimal' | undefined }
414
+ > = {
415
+ text: { type: 'text', inputMode: undefined },
416
+ numeric: { type: 'text', inputMode: 'numeric' },
417
+ decimal: { type: 'text', inputMode: 'decimal' },
418
+ tel: { type: 'tel', inputMode: undefined },
419
+ email: { type: 'email', inputMode: undefined },
420
+ url: { type: 'url', inputMode: undefined },
421
+ };
422
+
423
+ // The native control for a string field: the `inputMode` keyword OUTRANKS
424
+ // `format` when present; otherwise the format mapping stands as before. The value
425
+ // is always the typed string — `numeric`/`decimal` set only the keyboard, never a
426
+ // number coercion.
427
+ function stringControl(
428
+ format: string | undefined,
429
+ inputMode: FormInputMode | undefined,
430
+ ): { readonly type: string; readonly inputMode: 'numeric' | 'decimal' | undefined } {
431
+ if (inputMode !== undefined) return INPUT_MODE_CONTROLS[inputMode];
432
+ return { type: stringInputType(format), inputMode: undefined };
433
+ }
@@ -11,6 +11,24 @@
11
11
  export type JsonSchemaType =
12
12
  'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' | 'null';
13
13
 
14
+ /**
15
+ * The input KIND a `string` field collects, drawn as the matching keyboard or
16
+ * native control. The vocabulary is the HTML `inputmode`/control naming of the
17
+ * concept: `numeric`/`decimal` set a text box's keyboard hint; `tel`/`email`/`url`
18
+ * select the matching native control. It draws, it does not validate.
19
+ */
20
+ export type FormInputMode = 'text' | 'numeric' | 'decimal' | 'tel' | 'email' | 'url';
21
+
22
+ /**
23
+ * The CONTROL a surface draws for a property in place of the default for the
24
+ * property's shape. `chips` draws a choice (a `string` with an `enum`, or an
25
+ * `array` of such strings) as a tap-to-select chip grid instead of the
26
+ * radio/select or checkbox group. `textarea` draws a `string` as a multi-line
27
+ * text control instead of a single-line box. It changes what is DRAWN, never what
28
+ * is accepted — the value and its validation stay the property's own.
29
+ */
30
+ export type FormControl = 'chips' | 'textarea';
31
+
14
32
  /** A Pydantic-style discriminated-union tag: which property selects the variant. */
15
33
  export interface Discriminator {
16
34
  readonly propertyName: string;
@@ -61,6 +79,26 @@ export interface JsonSchema {
61
79
  readonly minItems?: number;
62
80
  readonly maxItems?: number;
63
81
  readonly pattern?: string;
82
+ /**
83
+ * The input KIND a `string` field collects — a platform keyword (see {@link
84
+ * FormInputMode}). The renderer draws the matching keyboard or native control:
85
+ * `numeric`/`decimal` a text box with that keyboard hint (the value stays the
86
+ * typed string, so `"007"` keeps its leading zeros), `tel`/`email`/`url` the
87
+ * matching native control, `text` or absent a plain text box. It DRAWS, it does
88
+ * not validate — the answer door still enforces `type`, `pattern` and `format`.
89
+ * When present it OUTRANKS `format` for the control. A value outside the set is
90
+ * read as absent (the server ask door is the authority that refuses it).
91
+ */
92
+ readonly inputMode?: FormInputMode;
93
+ /**
94
+ * The CONTROL to draw for this property in place of its shape's default — a
95
+ * platform keyword (see {@link FormControl}). `chips` draws a `string`-with-`enum`
96
+ * or an `array` of such strings as a chip grid; `textarea` draws a `string` as a
97
+ * multi-line text control. It DRAWS, it does not validate — the answer door still
98
+ * enforces the property's `type`, `enum`, `maxLength` and the rest. A value outside
99
+ * the set is read as absent (the server ask door is the authority that refuses it).
100
+ */
101
+ readonly control?: FormControl;
64
102
  /**
65
103
  * Platform date-constraint keys on a `string`/`date` (or `date-time`) property. They
66
104
  * are NOT the JSON-Schema numeric keywords (which are inert on a string): `minDate`/
@@ -9,12 +9,14 @@ it; holds no edge to jq (the jq fields render through the host's ambient
9
9
 
10
10
  - **template `input` jq** — input is the record (`.`), declared params bound as
11
11
  `$params`; returns the injected value.
12
- - **custom injection jq** — over `{ record, input }`; returns the value placed at `into`.
13
- - **adapter** — over `{ output, input }`; CONSTRUCTS the update jq's declared input
14
- object.
15
- - **template `update` jq** — over `{ record, input }` where `input` is the adapter's
16
- output; returns a template-relative op batch.
17
- - **custom `update` jq** — over `{ record, output, input }`; returns `[{op, path, value}]`.
12
+ - **custom injection jq** — over the record (`.`), the run input bound as `$input`; returns
13
+ the value placed at `into`.
14
+ - **adapter** — over the tool output (`.`), the run input bound as `$input`; CONSTRUCTS the
15
+ update jq's declared input object.
16
+ - **template `update` jq** — over the record subtree (`.`), the adapter's output bound as
17
+ `$input`; returns a template-relative op batch.
18
+ - **custom `update` jq** — over the tool output (`.`), the run input bound as `$input` and the
19
+ record as `$record`; returns `[{op, path, value}]`.
18
20
  - **`subject_expr`** — a jq yielding the record KEY: a bare key string, or a full subject object.
19
21
  - **`scope_expr`** — optional; a boolean jq predicate. When it is false, the state is skipped for the run.
20
22
 
@@ -28,7 +30,8 @@ canonical, parseable object so a stored adapter reopens as the form it was autho
28
30
  ```
29
31
 
30
32
  - `<key>` — a bare identifier, or a JSON string when it is not one.
31
- - `<value>` — one of: a field path (`.output…` / `.input…`), a JSON literal, or a raw
33
+ - `<value>` — one of: a field path (an output field `.…`, the whole output `.`, an input
34
+ field `$input…`), a JSON literal, or a raw
32
35
  jq expression, each wrapped in `(…)`.
33
36
 
34
37
  `parseAdapter` inverts it: `parseAdapter(compileAdapter(rows).jq) === rows` for every
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * The update list of one attached state. Each update applies AFTER the run: either
3
3
  * a template `update` jq (picked by name, whose declared input is authored through
4
- * the adapter mapping) or a custom jq over `{ record, output, input }` that authors
5
- * the whole op batch itself. An optional op-id expression carries idempotency.
4
+ * the adapter mapping) or a custom jq over the tool output (its `.`, with the run input
5
+ * bound as `$input` and the record as `$record`) that authors the whole op batch itself. An optional op-id expression carries idempotency.
6
6
  */
7
7
  import type { ReactNode } from 'react';
8
8
 
@@ -114,7 +114,7 @@ function UpdateRowEditor({
114
114
  <BindingTemplatedJqField
115
115
  label={`Custom update jq ${String(index + 1)}`}
116
116
  required
117
- description="A jq over `{ record, output, input }` returning a template-relative op batch."
117
+ description="A jq over the tool output (`.`), with `$input` and `$record` bound, returning a template-relative op batch."
118
118
  value={update.jq}
119
119
  onChange={(next) => {
120
120
  onChange({
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * A named template `update` jq declares an input object (its `params` / declared
5
5
  * input keys). A binding must map the run's OUTPUT (and the run's INPUT) into that
6
- * object; that mapping is the `adapter` — a single jq program over `{ output, input }`
7
- * that CONSTRUCTS the update jq's declared input (which the update jq then reads as
8
- * `.input` alongside the record).
6
+ * object; that mapping is the `adapter` — a single jq program over the tool output
7
+ * (its `.`) with the run input bound as `$input`, that CONSTRUCTS the update jq's
8
+ * declared input (which the update jq then reads as `$input`, over the record).
9
9
  *
10
10
  * The editor authors the adapter as a ROW form: one row per declared input key,
11
11
  * each row picking one of three value sources — a picked field (from the run's
@@ -17,10 +17,10 @@
17
17
  * object.
18
18
  */
19
19
 
20
- /** Which root of `{ output, input }` a picked field reads. */
20
+ /** Which value a picked field reads: the tool output (the program's `.`) or the run input (`$input`). */
21
21
  export type FieldRoot = 'output' | 'input';
22
22
 
23
- /** A picked field: a path into `output` or `input`. */
23
+ /** A picked field: a path into the tool output or the run input. */
24
24
  export interface FieldSource {
25
25
  readonly kind: 'field';
26
26
  readonly root: FieldRoot;
@@ -33,7 +33,7 @@ export interface LiteralSource {
33
33
  readonly json: string;
34
34
  }
35
35
 
36
- /** A raw jq expression over `{ output, input }`. */
36
+ /** A raw jq expression over the tool output (its `.`), with the run input bound as `$input`. */
37
37
  export interface JqSource {
38
38
  readonly kind: 'jq';
39
39
  readonly expr: string;
@@ -59,13 +59,19 @@ export function jqKey(key: string): string {
59
59
  return IDENTIFIER.test(key) ? key : JSON.stringify(key);
60
60
  }
61
61
 
62
- /** A picked field rendered as a jq path, e.g. `.output.user.name` / `.input["odd key"]`. */
62
+ /**
63
+ * A picked field rendered as a jq path: an output field reads off the program root
64
+ * (`.user.name`, `.["odd key"]`, the whole output `.`), an input field off the bound run
65
+ * input (`$input.user.name`, the whole input `$input`).
66
+ */
63
67
  export function fieldPathToJq(root: FieldRoot, path: readonly string[]): string {
64
- let out = `.${root}`;
68
+ let out = root === 'input' ? '$input' : '';
65
69
  for (const segment of path) {
66
- out += IDENTIFIER.test(segment) ? `.${segment}` : `[${JSON.stringify(segment)}]`;
70
+ out += IDENTIFIER.test(segment)
71
+ ? `.${segment}`
72
+ : `${out === '' ? '.' : ''}[${JSON.stringify(segment)}]`;
67
73
  }
68
- return out;
74
+ return out === '' ? '.' : out;
69
75
  }
70
76
 
71
77
  /** The jq fragment ONE row's value compiles to — the row's "show jq". */
@@ -95,7 +101,7 @@ export function rowValueJq(source: MappingSource): AdapterCompileResult {
95
101
 
96
102
  /**
97
103
  * Compile a mapping form into ONE adapter jq that constructs the update jq's
98
- * declared input over `{ output, input }`. An empty form compiles to the
104
+ * declared input over the tool output and `$input`. An empty form compiles to the
99
105
  * empty object `{}`; a blank target, a duplicate target, or an invalid source is a
100
106
  * loud error.
101
107
  */
@@ -128,7 +134,7 @@ export function defaultRowsForInput(params: readonly string[]): MappingRow[] {
128
134
  // `compileAdapter` emits a canonical, PARSEABLE shape so a stored adapter reopens as
129
135
  // the form it was authored in (never UI-only state): the object
130
136
  // `{ <key>: (<value>), … }` where `<key>` is a bare identifier or a JSON string and
131
- // `<value>` is one of — a field path `.output…` / `.input…`, a JSON literal, or a raw
137
+ // `<value>` is one of — a field path `.…` (output) / `$input…` (input), a JSON literal, or a raw
132
138
  // jq expression. `parseAdapter` inverts it; `parse(compileAdapter(rows).jq) === rows`
133
139
  // for every row kind whose literal is already canonical JSON. A shape it does not
134
140
  // recognise returns `null` — the editor falls back to the raw-jq escape hatch.
@@ -139,10 +145,19 @@ const ADAPTER_KEY = /^("(?:[^"\\]|\\.)*"|[A-Za-z_][A-Za-z0-9_]*)\s*:\s*\(/;
139
145
 
140
146
  /** Recover a field source from a pure path expression, or `null` if it is not one. */
141
147
  export function parseFieldPath(value: string): FieldSource | null {
142
- const head = /^\.(output|input)/.exec(value);
143
- if (head === null) return null;
144
- const root = head[1] as FieldRoot;
145
- let rest = value.slice(head[0].length);
148
+ let root: FieldRoot;
149
+ let rest: string;
150
+ if (value === '.') return { kind: 'field', root: 'output', path: [] };
151
+ if (value.startsWith('$input')) {
152
+ root = 'input';
153
+ rest = value.slice('$input'.length);
154
+ } else if (value.startsWith('.')) {
155
+ root = 'output';
156
+ // A root bracket (`.["odd key"]`) drops its leading dot; a root key keeps it (`.a`).
157
+ rest = value.startsWith('.[') ? value.slice(1) : value;
158
+ } else {
159
+ return null;
160
+ }
146
161
  const path: string[] = [];
147
162
  while (rest.length > 0) {
148
163
  if (rest.startsWith('.')) {