@tai42/studio-sdk 16.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 (317) 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 +2192 -2192
  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-form/SecretRefField.d.ts +2 -3
  196. package/dist/schema-form/SecretRefField.d.ts.map +1 -1
  197. package/dist/schema-form/SecretRefField.js +23 -12
  198. package/dist/schema-form/SecretRefField.js.map +1 -1
  199. package/dist/schema-form/classify.d.ts +1 -122
  200. package/dist/schema-form/classify.d.ts.map +1 -1
  201. package/dist/schema-form/classify.js +174 -237
  202. package/dist/schema-form/classify.js.map +1 -1
  203. package/dist/schema-form/default-value.d.ts.map +1 -1
  204. package/dist/schema-form/default-value.js +42 -19
  205. package/dist/schema-form/default-value.js.map +1 -1
  206. package/dist/schema-form/enum-field.d.ts +1 -1
  207. package/dist/schema-form/enum-field.d.ts.map +1 -1
  208. package/dist/schema-form/field-model.d.ts +121 -0
  209. package/dist/schema-form/field-model.d.ts.map +1 -0
  210. package/dist/schema-form/field-model.js +2 -0
  211. package/dist/schema-form/field-model.js.map +1 -0
  212. package/dist/schema-form/field-node.d.ts.map +1 -1
  213. package/dist/schema-form/field-node.js +25 -14
  214. package/dist/schema-form/field-node.js.map +1 -1
  215. package/dist/schema-form/media-field.d.ts +1 -1
  216. package/dist/schema-form/media-field.d.ts.map +1 -1
  217. package/dist/schema-form/media-field.js +47 -28
  218. package/dist/schema-form/media-field.js.map +1 -1
  219. package/dist/schema-form/string-annotations.d.ts +20 -0
  220. package/dist/schema-form/string-annotations.d.ts.map +1 -0
  221. package/dist/schema-form/string-annotations.js +105 -0
  222. package/dist/schema-form/string-annotations.js.map +1 -0
  223. package/dist/schema-form/string-field.d.ts +1 -1
  224. package/dist/schema-form/string-field.d.ts.map +1 -1
  225. package/dist/schema-form/union-field.d.ts +1 -1
  226. package/dist/schema-form/union-field.d.ts.map +1 -1
  227. package/dist/schema-form/union-field.js.map +1 -1
  228. package/dist/schema-form/union.d.ts +1 -1
  229. package/dist/schema-form/union.d.ts.map +1 -1
  230. package/dist/schema-form/validate.d.ts.map +1 -1
  231. package/dist/schema-form/validate.js +126 -105
  232. package/dist/schema-form/validate.js.map +1 -1
  233. package/dist/state-binding/AdapterMapping.d.ts.map +1 -1
  234. package/dist/state-binding/AdapterMapping.js +0 -0
  235. package/dist/state-binding/AdapterMapping.js.map +1 -1
  236. package/dist/state-binding/StateAttachRow.d.ts.map +1 -1
  237. package/dist/state-binding/StateAttachRow.js +52 -36
  238. package/dist/state-binding/StateAttachRow.js.map +1 -1
  239. package/dist/state-binding/adapter-mapping-row.d.ts +17 -0
  240. package/dist/state-binding/adapter-mapping-row.d.ts.map +1 -0
  241. package/dist/state-binding/adapter-mapping-row.js +87 -0
  242. package/dist/state-binding/adapter-mapping-row.js.map +1 -0
  243. package/dist/structured-output/StructuredOutput.d.ts.map +1 -1
  244. package/dist/structured-output/StructuredOutput.js +1 -3
  245. package/dist/structured-output/StructuredOutput.js.map +1 -1
  246. package/package.json +2 -2
  247. package/src/components/explorer-body.tsx +239 -0
  248. package/src/components/explorer-page-size.ts +39 -0
  249. package/src/components/explorer-tag-filter.tsx +80 -0
  250. package/src/components/explorer-tags.ts +64 -0
  251. package/src/components/explorer-view.tsx +114 -366
  252. package/src/components/extension-combo-builder.tsx +35 -152
  253. package/src/components/extension-combo-parts.tsx +237 -0
  254. package/src/components/fleet-report.tsx +97 -54
  255. package/src/components/icons/chrome-marks.tsx +195 -0
  256. package/src/components/icons/icon-frame.tsx +50 -0
  257. package/src/components/icons/index.ts +84 -0
  258. package/src/components/icons/nav-map.ts +55 -0
  259. package/src/components/icons/nav-marks.tsx +205 -0
  260. package/src/components/icons/status-marks.tsx +46 -0
  261. package/src/components/icons/theme-marks.tsx +33 -0
  262. package/src/components/json-diff.tsx +1 -4
  263. package/src/components/json-tree-context.ts +26 -0
  264. package/src/components/json-tree-model.ts +139 -0
  265. package/src/components/json-tree-nodes.tsx +224 -0
  266. package/src/components/json-tree.tsx +15 -434
  267. package/src/components/json-value-format.ts +64 -0
  268. package/src/components/markdown-blocks.ts +198 -0
  269. package/src/components/markdown-inline.tsx +365 -0
  270. package/src/components/markdown.tsx +5 -471
  271. package/src/components/observer-registry.ts +139 -0
  272. package/src/components/overflow-measure.ts +69 -0
  273. package/src/components/prose-regions.ts +153 -0
  274. package/src/components/scroll-region.tsx +20 -360
  275. package/src/components/tab-stop-parse.ts +217 -0
  276. package/src/components/templated-text-field.tsx +38 -330
  277. package/src/components/templated-text-kwargs-editor.tsx +147 -0
  278. package/src/components/templated-text-kwargs.ts +92 -0
  279. package/src/components/templated-text-source.tsx +226 -0
  280. package/src/components/tool-picker-parts.tsx +184 -0
  281. package/src/components/tool-picker.tsx +22 -114
  282. package/src/components/version-history-panel.tsx +38 -142
  283. package/src/components/version-history-rows.tsx +220 -0
  284. package/src/guards.ts +7 -0
  285. package/src/hooks/interactions-connection.ts +227 -0
  286. package/src/hooks/interactions-stream.ts +274 -0
  287. package/src/hooks/useSse.ts +50 -477
  288. package/src/navigation/app-link.tsx +70 -0
  289. package/src/navigation/context.tsx +11 -330
  290. package/src/navigation/entry-state.ts +42 -0
  291. package/src/navigation/guard-registry.ts +65 -0
  292. package/src/navigation/guarded-navigation.ts +147 -0
  293. package/src/navigation/index.ts +2 -2
  294. package/src/navigation/types.ts +8 -11
  295. package/src/navigation/use-back-forward-guard.ts +90 -0
  296. package/src/plugin/registry.ts +111 -90
  297. package/src/schema-form/SecretRefField.tsx +121 -71
  298. package/src/schema-form/classify.ts +244 -395
  299. package/src/schema-form/default-value.ts +48 -17
  300. package/src/schema-form/enum-field.tsx +1 -1
  301. package/src/schema-form/field-model.ts +133 -0
  302. package/src/schema-form/field-node.tsx +94 -38
  303. package/src/schema-form/media-field.tsx +134 -65
  304. package/src/schema-form/string-annotations.ts +111 -0
  305. package/src/schema-form/string-field.tsx +1 -1
  306. package/src/schema-form/union-field.tsx +2 -1
  307. package/src/schema-form/union.ts +1 -1
  308. package/src/schema-form/validate.ts +184 -103
  309. package/src/state-binding/AdapterMapping.tsx +0 -0
  310. package/src/state-binding/StateAttachRow.tsx +222 -147
  311. package/src/state-binding/adapter-mapping-row.tsx +232 -0
  312. package/src/structured-output/StructuredOutput.tsx +1 -4
  313. package/dist/components/icons.d.ts +0 -160
  314. package/dist/components/icons.d.ts.map +0 -1
  315. package/dist/components/icons.js +0 -160
  316. package/dist/components/icons.js.map +0 -1
  317. package/src/components/icons.tsx +0 -586
@@ -34,25 +34,21 @@
34
34
  *
35
35
  * SAFETY: an extension name is server-supplied, so every name renders as TEXT through
36
36
  * the DS `Badge`/`Checkbox` (React escapes it) — never an HTML sink.
37
- *
38
- * Geometry and ink come from the design-system classes: each committed combo is a
39
- * `tai-card` row, the invalid notes are `tai-field-error` (icon + message, never
40
- * color alone), and Remove is a labelled `Button` carrying `CloseIcon` beside the
41
- * word, under an accessible name that names the combo it removes.
42
37
  */
43
- import { Fragment, useEffect, useState, type ReactNode } from 'react';
38
+ import { useEffect, useState, type ReactNode } from 'react';
44
39
 
45
40
  import type { Extension, PresetExtensionElement } from '@tai42/api-client';
46
41
 
47
42
  import { comboElementNames, extensionElementName } from '../extension-combos';
48
- import { SchemaEditor, type SchemaEditorChange } from '../schema-editor';
49
- import { Badge } from './badge';
50
- import { ExtensionPicker } from './extension-picker';
51
- import { CloseIcon, XCircleIcon } from './icons';
52
- import { Button } from './primitives';
53
-
54
- /** The one config-taking extension this builder authors inline. */
55
- const OUTPUT_SCHEMA = 'output_schema';
43
+ import { type SchemaEditorChange } from '../schema-editor';
44
+ import {
45
+ CommittedComboList,
46
+ ComboDraftEditor,
47
+ OUTPUT_SCHEMA,
48
+ outputSchemaConfig,
49
+ sameNames,
50
+ unknownNames,
51
+ } from './extension-combo-parts';
56
52
 
57
53
  export interface ExtensionComboBuilderProps {
58
54
  /** The extension catalog the combos are drawn from (caller-fetched). */
@@ -79,34 +75,6 @@ export interface ExtensionComboBuilderProps {
79
75
  readonly availableReady?: boolean;
80
76
  }
81
77
 
82
- /** Two combos are equal when their NAME sequences match (config is not part of identity). */
83
- function sameNames(a: readonly string[], b: readonly string[]): boolean {
84
- return a.length === b.length && a.every((name, index) => name === b[index]);
85
- }
86
-
87
- /** The stored `output_schema` config schema for a combo, if it carries one. */
88
- function outputSchemaConfig(
89
- combo: readonly PresetExtensionElement[],
90
- ): Record<string, unknown> | null {
91
- for (const element of combo) {
92
- if (typeof element !== 'string' && element.name === OUTPUT_SCHEMA) {
93
- const schema = element.config.schema;
94
- return typeof schema === 'object' && schema !== null && !Array.isArray(schema)
95
- ? (schema as Record<string, unknown>)
96
- : {};
97
- }
98
- }
99
- return null;
100
- }
101
-
102
- /** The element names in a combo that are absent from the known catalog. */
103
- function unknownNames(
104
- combo: readonly PresetExtensionElement[],
105
- availableNames: ReadonlySet<string>,
106
- ): string[] {
107
- return comboElementNames(combo).filter((name) => !availableNames.has(name));
108
- }
109
-
110
78
  export function ExtensionComboBuilder({
111
79
  available,
112
80
  value,
@@ -192,116 +160,31 @@ export function ExtensionComboBuilder({
192
160
 
193
161
  return (
194
162
  <div data-testid={idPrefix} className="tai-stack">
195
- {value.length > 0 ? (
196
- <ul className="tai-stack tai-stack-2">
197
- {value.map((combo, index) => {
198
- const names = comboElementNames(combo);
199
- const unknown = availableReady ? unknownNames(combo, availableNames) : [];
200
- return (
201
- // The index IS the identity here: the list mutates by append, in-place
202
- // update, and remove-by-index (no reorder), and a combo has no stable id
203
- // of its own — so an index key is stable and cannot collide.
204
- <li key={index} className="tai-card tai-stack tai-stack-2">
205
- <div className="tai-row">
206
- {/* The badges take the row's spare width so the Edit/Remove pair
207
- pins to a stable trailing edge across combos of varied length. */}
208
- <span className="tai-row" style={{ flex: 1 }}>
209
- {names.map((name, position) => (
210
- <Fragment key={`${name}-${String(position)}`}>
211
- {position > 0 ? (
212
- <span aria-hidden className="tai-muted">
213
- +
214
- </span>
215
- ) : null}
216
- <Badge variant={unknown.includes(name) ? 'danger' : 'primary'}>
217
- {name}
218
- </Badge>
219
- </Fragment>
220
- ))}
221
- </span>
222
- <Button
223
- type="button"
224
- // The name starts with the words the button is SHOWING, which
225
- // is what WCAG 2.5.3 (Label in Name) asks: a constant "Edit …"
226
- // would leave a button reading "Editing" named "Edit", and a
227
- // voice-control user naming a control they cannot see.
228
- aria-label={`${editing === index ? 'Editing' : 'Edit'} extension set ${names.join('+')}`}
229
- disabled={disabled === true || editing === index}
230
- onClick={() => {
231
- startEdit(index);
232
- }}
233
- >
234
- {editing === index ? 'Editing' : 'Edit'}
235
- </Button>
236
- <Button
237
- type="button"
238
- aria-label={`Remove extension set ${names.join('+')}`}
239
- disabled={disabled}
240
- onClick={() => {
241
- removeCombo(index);
242
- }}
243
- >
244
- <CloseIcon />
245
- Remove
246
- </Button>
247
- </div>
248
- {unknown.length > 0 ? (
249
- <p role="alert" className="tai-field-error">
250
- <XCircleIcon />
251
- {unknown.length === 1 ? 'Unknown extension: ' : 'Unknown extensions: '}
252
- {unknown.join(', ')}.
253
- </p>
254
- ) : null}
255
- </li>
256
- );
257
- })}
258
- </ul>
259
- ) : (
260
- <p className="tai-muted">No extension sets.</p>
261
- )}
262
-
263
- <div className="tai-stack tai-stack-2">
264
- <ExtensionPicker
265
- available={available}
266
- value={draft}
267
- onChange={setDraft}
268
- disabled={disabled}
269
- idPrefix={`${idPrefix}-draft`}
270
- />
271
-
272
- {draftHasOutputSchema ? (
273
- <SchemaEditor
274
- value={draftConfig.schema}
275
- onChange={setDraftConfig}
276
- requireTitle={false}
277
- label="Output schema"
278
- description="The JSON Schema the output_schema extension enforces on this extension set's result."
279
- disabled={disabled}
280
- idPrefix={`${idPrefix}-output-schema`}
281
- // Re-seed the editor when the draft target changes (a new draft vs a
282
- // specific combo pulled in for edit).
283
- key={editing === null ? 'new' : `edit-${String(editing)}`}
284
- />
285
- ) : null}
286
-
287
- {isDuplicate ? (
288
- <p role="alert" className="tai-field-error">
289
- <XCircleIcon />
290
- This extension set is already added.
291
- </p>
292
- ) : null}
293
-
294
- <div className="tai-row">
295
- <Button type="button" variant="primary" onClick={commitCombo} disabled={!canAdd}>
296
- {editing === null ? 'Add extension set' : 'Update extension set'}
297
- </Button>
298
- {editing !== null ? (
299
- <Button type="button" onClick={resetDraft} disabled={disabled}>
300
- Cancel edit
301
- </Button>
302
- ) : null}
303
- </div>
304
- </div>
163
+ <CommittedComboList
164
+ value={value}
165
+ availableReady={availableReady}
166
+ availableNames={availableNames}
167
+ editing={editing}
168
+ disabled={disabled}
169
+ onEdit={startEdit}
170
+ onRemove={removeCombo}
171
+ />
172
+
173
+ <ComboDraftEditor
174
+ available={available}
175
+ draft={draft}
176
+ onDraftChange={setDraft}
177
+ draftHasOutputSchema={draftHasOutputSchema}
178
+ draftConfig={draftConfig}
179
+ onDraftConfigChange={setDraftConfig}
180
+ isDuplicate={isDuplicate}
181
+ canAdd={canAdd}
182
+ editing={editing}
183
+ disabled={disabled}
184
+ idPrefix={idPrefix}
185
+ onCommit={commitCombo}
186
+ onReset={resetDraft}
187
+ />
305
188
  </div>
306
189
  );
307
190
  }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * The pieces of `ExtensionComboBuilder`: the committed-combo list
3
+ * (`CommittedComboList`), the under-construction combo editor (`ComboDraftEditor`),
4
+ * and the shared combo helpers. The builder owns the state and composes these.
5
+ */
6
+ import { Fragment, type ReactNode } from 'react';
7
+
8
+ import type { Extension, PresetExtensionElement } from '@tai42/api-client';
9
+
10
+ import { comboElementNames } from '../extension-combos';
11
+ import { SchemaEditor, type SchemaEditorChange } from '../schema-editor';
12
+ import { Badge } from './badge';
13
+ import { ExtensionPicker } from './extension-picker';
14
+ import { CloseIcon, XCircleIcon } from './icons';
15
+ import { Button } from './primitives';
16
+
17
+ /** The one config-taking extension this builder authors inline. */
18
+ export const OUTPUT_SCHEMA = 'output_schema';
19
+
20
+ /** Two combos are equal when their NAME sequences match (config is not part of identity). */
21
+ export function sameNames(a: readonly string[], b: readonly string[]): boolean {
22
+ return a.length === b.length && a.every((name, index) => name === b[index]);
23
+ }
24
+
25
+ /** The stored `output_schema` config schema for a combo, if it carries one. */
26
+ export function outputSchemaConfig(
27
+ combo: readonly PresetExtensionElement[],
28
+ ): Record<string, unknown> | null {
29
+ for (const element of combo) {
30
+ if (typeof element !== 'string' && element.name === OUTPUT_SCHEMA) {
31
+ const schema = element.config.schema;
32
+ return typeof schema === 'object' && schema !== null && !Array.isArray(schema)
33
+ ? (schema as Record<string, unknown>)
34
+ : {};
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+
40
+ /** The element names in a combo that are absent from the known catalog. */
41
+ export function unknownNames(
42
+ combo: readonly PresetExtensionElement[],
43
+ availableNames: ReadonlySet<string>,
44
+ ): string[] {
45
+ return comboElementNames(combo).filter((name) => !availableNames.has(name));
46
+ }
47
+
48
+ /** One committed combo's row: its name badges and the Edit / Remove pair, with an
49
+ * inline unknown-name note. */
50
+ function CommittedComboRow({
51
+ combo,
52
+ index,
53
+ editing,
54
+ disabled,
55
+ unknown,
56
+ onEdit,
57
+ onRemove,
58
+ }: {
59
+ readonly combo: readonly PresetExtensionElement[];
60
+ readonly index: number;
61
+ readonly editing: number | null;
62
+ readonly disabled: boolean | undefined;
63
+ readonly unknown: readonly string[];
64
+ readonly onEdit: (index: number) => void;
65
+ readonly onRemove: (index: number) => void;
66
+ }): ReactNode {
67
+ const names = comboElementNames(combo);
68
+ return (
69
+ <li className="tai-card tai-stack tai-stack-2">
70
+ <div className="tai-row">
71
+ {/* The badges take the row's spare width so the Edit/Remove pair pins to a
72
+ stable trailing edge across combos of varied length. */}
73
+ <span className="tai-row" style={{ flex: 1 }}>
74
+ {names.map((name, position) => (
75
+ <Fragment key={`${name}-${String(position)}`}>
76
+ {position > 0 ? (
77
+ <span aria-hidden className="tai-muted">
78
+ +
79
+ </span>
80
+ ) : null}
81
+ <Badge variant={unknown.includes(name) ? 'danger' : 'primary'}>{name}</Badge>
82
+ </Fragment>
83
+ ))}
84
+ </span>
85
+ <Button
86
+ type="button"
87
+ // The name starts with the words the button is SHOWING, which is what WCAG
88
+ // 2.5.3 (Label in Name) asks: a constant "Edit …" would leave a button
89
+ // reading "Editing" named "Edit", and a voice-control user naming a control
90
+ // they cannot see.
91
+ aria-label={`${editing === index ? 'Editing' : 'Edit'} extension set ${names.join('+')}`}
92
+ disabled={disabled === true || editing === index}
93
+ onClick={() => {
94
+ onEdit(index);
95
+ }}
96
+ >
97
+ {editing === index ? 'Editing' : 'Edit'}
98
+ </Button>
99
+ <Button
100
+ type="button"
101
+ aria-label={`Remove extension set ${names.join('+')}`}
102
+ disabled={disabled}
103
+ onClick={() => {
104
+ onRemove(index);
105
+ }}
106
+ >
107
+ <CloseIcon />
108
+ Remove
109
+ </Button>
110
+ </div>
111
+ {unknown.length > 0 ? (
112
+ <p role="alert" className="tai-field-error">
113
+ <XCircleIcon />
114
+ {unknown.length === 1 ? 'Unknown extension: ' : 'Unknown extensions: '}
115
+ {unknown.join(', ')}.
116
+ </p>
117
+ ) : null}
118
+ </li>
119
+ );
120
+ }
121
+
122
+ /** The current combos, each a row of name badges with per-combo Edit and Remove. */
123
+ export function CommittedComboList({
124
+ value,
125
+ availableReady,
126
+ availableNames,
127
+ editing,
128
+ disabled,
129
+ onEdit,
130
+ onRemove,
131
+ }: {
132
+ readonly value: readonly PresetExtensionElement[][];
133
+ readonly availableReady: boolean;
134
+ readonly availableNames: ReadonlySet<string>;
135
+ readonly editing: number | null;
136
+ readonly disabled: boolean | undefined;
137
+ readonly onEdit: (index: number) => void;
138
+ readonly onRemove: (index: number) => void;
139
+ }): ReactNode {
140
+ if (value.length === 0) return <p className="tai-muted">No extension sets.</p>;
141
+ return (
142
+ <ul className="tai-stack tai-stack-2">
143
+ {value.map((combo, index) => (
144
+ // The index IS the identity here: the list mutates by append, in-place update,
145
+ // and remove-by-index (no reorder), and a combo has no stable id of its own —
146
+ // so an index key is stable and cannot collide.
147
+ <CommittedComboRow
148
+ key={index}
149
+ combo={combo}
150
+ index={index}
151
+ editing={editing}
152
+ disabled={disabled}
153
+ unknown={availableReady ? unknownNames(combo, availableNames) : []}
154
+ onEdit={onEdit}
155
+ onRemove={onRemove}
156
+ />
157
+ ))}
158
+ </ul>
159
+ );
160
+ }
161
+
162
+ /** The combo under construction: the extension picker, the inline output-schema
163
+ * editor when present, the duplicate note, and the Add/Update + Cancel actions. */
164
+ export function ComboDraftEditor({
165
+ available,
166
+ draft,
167
+ onDraftChange,
168
+ draftHasOutputSchema,
169
+ draftConfig,
170
+ onDraftConfigChange,
171
+ isDuplicate,
172
+ canAdd,
173
+ editing,
174
+ disabled,
175
+ idPrefix,
176
+ onCommit,
177
+ onReset,
178
+ }: {
179
+ readonly available: readonly Extension[];
180
+ readonly draft: string[];
181
+ readonly onDraftChange: (draft: string[]) => void;
182
+ readonly draftHasOutputSchema: boolean;
183
+ readonly draftConfig: SchemaEditorChange;
184
+ readonly onDraftConfigChange: (change: SchemaEditorChange) => void;
185
+ readonly isDuplicate: boolean;
186
+ readonly canAdd: boolean;
187
+ readonly editing: number | null;
188
+ readonly disabled: boolean | undefined;
189
+ readonly idPrefix: string;
190
+ readonly onCommit: () => void;
191
+ readonly onReset: () => void;
192
+ }): ReactNode {
193
+ return (
194
+ <div className="tai-stack tai-stack-2">
195
+ <ExtensionPicker
196
+ available={available}
197
+ value={draft}
198
+ onChange={onDraftChange}
199
+ disabled={disabled}
200
+ idPrefix={`${idPrefix}-draft`}
201
+ />
202
+
203
+ {draftHasOutputSchema ? (
204
+ <SchemaEditor
205
+ value={draftConfig.schema}
206
+ onChange={onDraftConfigChange}
207
+ requireTitle={false}
208
+ label="Output schema"
209
+ description="The JSON Schema the output_schema extension enforces on this extension set's result."
210
+ disabled={disabled}
211
+ idPrefix={`${idPrefix}-output-schema`}
212
+ // Re-seed the editor when the draft target changes (a new draft vs a
213
+ // specific combo pulled in for edit).
214
+ key={editing === null ? 'new' : `edit-${String(editing)}`}
215
+ />
216
+ ) : null}
217
+
218
+ {isDuplicate ? (
219
+ <p role="alert" className="tai-field-error">
220
+ <XCircleIcon />
221
+ This extension set is already added.
222
+ </p>
223
+ ) : null}
224
+
225
+ <div className="tai-row">
226
+ <Button type="button" variant="primary" onClick={onCommit} disabled={!canAdd}>
227
+ {editing === null ? 'Add extension set' : 'Update extension set'}
228
+ </Button>
229
+ {editing !== null ? (
230
+ <Button type="button" onClick={onReset} disabled={disabled}>
231
+ Cancel edit
232
+ </Button>
233
+ ) : null}
234
+ </div>
235
+ </div>
236
+ );
237
+ }
@@ -28,7 +28,11 @@
28
28
  * report is readable without color perception.
29
29
  */
30
30
  import type { ReactNode } from 'react';
31
- import type { FleetReportSummary, FleetFailureOutcome } from '@tai42/api-client';
31
+ import type {
32
+ FleetReportSummary,
33
+ FleetFailureOutcome,
34
+ FleetWorkerFailure,
35
+ } from '@tai42/api-client';
32
36
 
33
37
  import { AlertTriangleIcon, PendingIcon, XCircleIcon, type IconComponent } from './icons';
34
38
 
@@ -86,62 +90,75 @@ export interface FleetReportProps {
86
90
  readonly action?: 'save' | 'reload' | 'deregister' | 'remove';
87
91
  }
88
92
 
89
- export function FleetReport({ summary, action = 'save' }: FleetReportProps): ReactNode {
90
- if (summary === null || summary.status === 'converged') return null;
91
-
92
- // Only a saved config change carries the "Change saved" framing; a reload or a
93
- // deregister saved nothing.
94
- const savedFraming = action === 'save';
93
+ type FleetAction = NonNullable<FleetReportProps['action']>;
95
94
 
96
- if (summary.status === 'unreachable') {
97
- // Where to send the operator once the bus is back: re-run the same op, except a
98
- // saved change reloads from the System page.
99
- const unreachableRemediation =
100
- action === 'deregister'
101
- ? 'Re-run the deregister once the bus is back.'
102
- : action === 'remove'
103
- ? 'Re-run the remove once the bus is back.'
104
- : action === 'reload'
105
- ? 'Re-run the reload once the bus is back.'
106
- : 'Re-run the reload from the System page once the bus is back.';
107
- return (
108
- <div role="alert" className="tai-error-state tai-stack tai-stack-2">
109
- <strong className="tai-status tai-status-err">
110
- <XCircleIcon />
111
- {savedFraming
112
- ? 'Change saved, but the worker fleet was not reached'
113
- : 'The worker fleet was not reached'}
114
- </strong>
115
- <p>
116
- {`The worker bus was unreachable, so other workers may still be running the old config. ${unreachableRemediation}`}
117
- </p>
118
- {/* The bus error is a message, not a listing: it wraps rather than scrolls. */}
119
- {summary.error !== null ? (
120
- <p className="tai-code-block tai-code-block-wrap">{summary.error}</p>
121
- ) : null}
122
- </div>
123
- );
95
+ /**
96
+ * Where to send the operator, per action and phase. On `unreachable` the copy
97
+ * is a full sentence ending "once the bus is back."; on `degraded` it is the
98
+ * verb phrase spliced into the sentence the panel builds. A saved change reloads
99
+ * from the System page; a detach re-runs itself, since a reload would re-attach.
100
+ */
101
+ function remediationText(action: FleetAction, phase: FleetReportStatusPhase): string {
102
+ if (phase === 'unreachable') {
103
+ if (action === 'deregister') return 'Re-run the deregister once the bus is back.';
104
+ if (action === 'remove') return 'Re-run the remove once the bus is back.';
105
+ if (action === 'reload') return 'Re-run the reload once the bus is back.';
106
+ return 'Re-run the reload from the System page once the bus is back.';
124
107
  }
108
+ if (action === 'deregister') return 're-run the deregister';
109
+ if (action === 'remove') return 're-run the remove';
110
+ if (action === 'reload') return 're-run the reload';
111
+ return 'reload the fleet from the System page';
112
+ }
113
+
114
+ type FleetReportStatusPhase = 'unreachable' | 'degraded';
115
+
116
+ /** The loud panel for a bus that could not be reached: no sibling was told. */
117
+ function UnreachableReport({
118
+ error,
119
+ savedFraming,
120
+ remediation,
121
+ }: {
122
+ readonly error: string | null;
123
+ readonly savedFraming: boolean;
124
+ readonly remediation: string;
125
+ }): ReactNode {
126
+ return (
127
+ <div role="alert" className="tai-error-state tai-stack tai-stack-2">
128
+ <strong className="tai-status tai-status-err">
129
+ <XCircleIcon />
130
+ {savedFraming
131
+ ? 'Change saved, but the worker fleet was not reached'
132
+ : 'The worker fleet was not reached'}
133
+ </strong>
134
+ <p>
135
+ {`The worker bus was unreachable, so other workers may still be running the old config. ${remediation}`}
136
+ </p>
137
+ {/* The bus error is a message, not a listing: it wraps rather than scrolls. */}
138
+ {error !== null ? <p className="tai-code-block tai-code-block-wrap">{error}</p> : null}
139
+ </div>
140
+ );
141
+ }
125
142
 
126
- // degraded — the bus was reached but named workers did not converge. That is a
127
- // warning, not a failure, so it takes the warning surface: a warn-toned
128
- // headline inside an error-toned panel would state two different severities.
129
- //
130
- // A count and its noun agree. `worker(s)` is machine output at the one moment
131
- // an operator is being told something went wrong, and a single unconverged
132
- // worker is the commonest degraded fleet there is.
133
- const count = summary.failures.length;
143
+ /**
144
+ * The loud panel for a reachable bus whose named workers did not converge. A
145
+ * warning, not a failure, so it takes the warning surface: a warn-toned headline
146
+ * inside an error-toned panel would state two different severities.
147
+ */
148
+ function DegradedReport({
149
+ failures,
150
+ savedFraming,
151
+ remediation,
152
+ }: {
153
+ readonly failures: readonly FleetWorkerFailure[];
154
+ readonly savedFraming: boolean;
155
+ readonly remediation: string;
156
+ }): ReactNode {
157
+ // A count and its noun agree. `worker(s)` is machine output at the one moment an
158
+ // operator is being told something went wrong, and a single unconverged worker
159
+ // is the commonest degraded fleet there is.
160
+ const count = failures.length;
134
161
  const workers = `${String(count)} worker${count === 1 ? '' : 's'}`;
135
- // How the operator converges the stranded workers: re-run the same op, except a
136
- // saved change reloads the fleet from the System page.
137
- const remediation =
138
- action === 'deregister'
139
- ? 're-run the deregister'
140
- : action === 'remove'
141
- ? 're-run the remove'
142
- : action === 'reload'
143
- ? 're-run the reload'
144
- : 'reload the fleet from the System page';
145
162
  return (
146
163
  <div role="alert" className="tai-warn-state tai-stack tai-stack-2">
147
164
  <strong className="tai-status tai-status-warn">
@@ -156,7 +173,7 @@ export function FleetReport({ summary, action = 'save' }: FleetReportProps): Rea
156
173
  ` to converge ${count === 1 ? 'it' : 'them'}.`}
157
174
  </p>
158
175
  <ul className="tai-stack tai-stack-2">
159
- {summary.failures.map((failure) => {
176
+ {failures.map((failure) => {
160
177
  const { tone, Icon } = OUTCOME_STATUS[failure.outcome];
161
178
  return (
162
179
  <li key={failure.name} className="tai-row">
@@ -175,3 +192,29 @@ export function FleetReport({ summary, action = 'save' }: FleetReportProps): Rea
175
192
  </div>
176
193
  );
177
194
  }
195
+
196
+ export function FleetReport({ summary, action = 'save' }: FleetReportProps): ReactNode {
197
+ if (summary === null || summary.status === 'converged') return null;
198
+
199
+ // Only a saved config change carries the "Change saved" framing; a reload or a
200
+ // deregister saved nothing.
201
+ const savedFraming = action === 'save';
202
+
203
+ if (summary.status === 'unreachable') {
204
+ return (
205
+ <UnreachableReport
206
+ error={summary.error}
207
+ savedFraming={savedFraming}
208
+ remediation={remediationText(action, 'unreachable')}
209
+ />
210
+ );
211
+ }
212
+
213
+ return (
214
+ <DegradedReport
215
+ failures={summary.failures}
216
+ savedFraming={savedFraming}
217
+ remediation={remediationText(action, 'degraded')}
218
+ />
219
+ );
220
+ }