@tai42/studio-sdk 25.0.1 → 27.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 (262) hide show
  1. package/README.md +143 -26
  2. package/dist/components/action-menu.d.ts +30 -0
  3. package/dist/components/action-menu.d.ts.map +1 -0
  4. package/dist/components/action-menu.js +101 -0
  5. package/dist/components/action-menu.js.map +1 -0
  6. package/dist/components/agent-picker.d.ts +22 -0
  7. package/dist/components/agent-picker.d.ts.map +1 -0
  8. package/dist/components/agent-picker.js +17 -0
  9. package/dist/components/agent-picker.js.map +1 -0
  10. package/dist/components/channel-picker.d.ts +27 -0
  11. package/dist/components/channel-picker.d.ts.map +1 -0
  12. package/dist/components/channel-picker.js +26 -0
  13. package/dist/components/channel-picker.js.map +1 -0
  14. package/dist/components/components.css +226 -8
  15. package/dist/components/confirm-dialog.d.ts +3 -1
  16. package/dist/components/confirm-dialog.d.ts.map +1 -1
  17. package/dist/components/confirm-dialog.js +2 -2
  18. package/dist/components/confirm-dialog.js.map +1 -1
  19. package/dist/components/execution-key-picker.d.ts +26 -10
  20. package/dist/components/execution-key-picker.d.ts.map +1 -1
  21. package/dist/components/execution-key-picker.js +134 -15
  22. package/dist/components/execution-key-picker.js.map +1 -1
  23. package/dist/components/extension-combo-builder.d.ts +7 -7
  24. package/dist/components/extension-combo-builder.d.ts.map +1 -1
  25. package/dist/components/extension-combo-builder.js +42 -16
  26. package/dist/components/extension-combo-builder.js.map +1 -1
  27. package/dist/components/extension-combo-parts.d.ts +13 -11
  28. package/dist/components/extension-combo-parts.d.ts.map +1 -1
  29. package/dist/components/extension-combo-parts.js +10 -19
  30. package/dist/components/extension-combo-parts.js.map +1 -1
  31. package/dist/components/extension-config-editor.d.ts +33 -0
  32. package/dist/components/extension-config-editor.d.ts.map +1 -0
  33. package/dist/components/extension-config-editor.js +190 -0
  34. package/dist/components/extension-config-editor.js.map +1 -0
  35. package/dist/components/extension-grouping.d.ts +4 -41
  36. package/dist/components/extension-grouping.d.ts.map +1 -1
  37. package/dist/components/extension-grouping.js +1 -48
  38. package/dist/components/extension-grouping.js.map +1 -1
  39. package/dist/components/extension-picker.d.ts +4 -3
  40. package/dist/components/extension-picker.d.ts.map +1 -1
  41. package/dist/components/extension-picker.js +6 -6
  42. package/dist/components/extension-picker.js.map +1 -1
  43. package/dist/components/fleet-report.d.ts +8 -3
  44. package/dist/components/fleet-report.d.ts.map +1 -1
  45. package/dist/components/fleet-report.js +12 -2
  46. package/dist/components/fleet-report.js.map +1 -1
  47. package/dist/components/holder-kinds.d.ts +18 -0
  48. package/dist/components/holder-kinds.d.ts.map +1 -0
  49. package/dist/components/holder-kinds.js +41 -0
  50. package/dist/components/holder-kinds.js.map +1 -0
  51. package/dist/components/holder-list.d.ts +58 -0
  52. package/dist/components/holder-list.d.ts.map +1 -0
  53. package/dist/components/holder-list.js +77 -0
  54. package/dist/components/holder-list.js.map +1 -0
  55. package/dist/components/icons/index.d.ts +1 -1
  56. package/dist/components/icons/index.d.ts.map +1 -1
  57. package/dist/components/icons/index.js +1 -1
  58. package/dist/components/icons/index.js.map +1 -1
  59. package/dist/components/icons/nav-map.d.ts.map +1 -1
  60. package/dist/components/icons/nav-map.js +1 -3
  61. package/dist/components/icons/nav-map.js.map +1 -1
  62. package/dist/components/icons/nav-marks.d.ts +0 -4
  63. package/dist/components/icons/nav-marks.d.ts.map +1 -1
  64. package/dist/components/icons/nav-marks.js +0 -4
  65. package/dist/components/icons/nav-marks.js.map +1 -1
  66. package/dist/components/named-list-picker.d.ts +39 -0
  67. package/dist/components/named-list-picker.d.ts.map +1 -0
  68. package/dist/components/named-list-picker.js +61 -0
  69. package/dist/components/named-list-picker.js.map +1 -0
  70. package/dist/components/named-list-test-utils.d.ts +23 -0
  71. package/dist/components/named-list-test-utils.d.ts.map +1 -0
  72. package/dist/components/named-list-test-utils.js +33 -0
  73. package/dist/components/named-list-test-utils.js.map +1 -0
  74. package/dist/components/recycle-roll.d.ts +23 -0
  75. package/dist/components/recycle-roll.d.ts.map +1 -0
  76. package/dist/components/recycle-roll.js +47 -0
  77. package/dist/components/recycle-roll.js.map +1 -0
  78. package/dist/components/stepper.d.ts +28 -0
  79. package/dist/components/stepper.d.ts.map +1 -0
  80. package/dist/components/stepper.js +33 -0
  81. package/dist/components/stepper.js.map +1 -0
  82. package/dist/components/subject-section.d.ts +2 -1
  83. package/dist/components/subject-section.d.ts.map +1 -1
  84. package/dist/components/subject-section.js +10 -5
  85. package/dist/components/subject-section.js.map +1 -1
  86. package/dist/components/tokens.css +31 -41
  87. package/dist/components/tokens.d.ts +3 -4
  88. package/dist/components/tokens.d.ts.map +1 -1
  89. package/dist/components/tokens.js +5 -13
  90. package/dist/components/tokens.js.map +1 -1
  91. package/dist/components/tool-picker-parts.d.ts +5 -1
  92. package/dist/components/tool-picker-parts.d.ts.map +1 -1
  93. package/dist/components/tool-picker-parts.js +8 -6
  94. package/dist/components/tool-picker-parts.js.map +1 -1
  95. package/dist/components/tool-picker.d.ts +77 -2
  96. package/dist/components/tool-picker.d.ts.map +1 -1
  97. package/dist/components/tool-picker.js +87 -29
  98. package/dist/components/tool-picker.js.map +1 -1
  99. package/dist/components/version-history-panel.d.ts +7 -1
  100. package/dist/components/version-history-panel.d.ts.map +1 -1
  101. package/dist/components/version-history-panel.js +2 -2
  102. package/dist/components/version-history-panel.js.map +1 -1
  103. package/dist/components/version-history-rows.d.ts +4 -2
  104. package/dist/components/version-history-rows.d.ts.map +1 -1
  105. package/dist/components/version-history-rows.js +14 -2
  106. package/dist/components/version-history-rows.js.map +1 -1
  107. package/dist/errors.d.ts +5 -0
  108. package/dist/errors.d.ts.map +1 -1
  109. package/dist/errors.js +7 -0
  110. package/dist/errors.js.map +1 -1
  111. package/dist/hooks/useApi.d.ts +524 -197
  112. package/dist/hooks/useApi.d.ts.map +1 -1
  113. package/dist/hooks/useCapabilities.d.ts +43 -10
  114. package/dist/hooks/useCapabilities.d.ts.map +1 -1
  115. package/dist/hooks/useCapabilities.js +39 -10
  116. package/dist/hooks/useCapabilities.js.map +1 -1
  117. package/dist/index.d.ts +25 -12
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +14 -6
  120. package/dist/index.js.map +1 -1
  121. package/dist/marketplace-install/env-var-fields.d.ts +17 -0
  122. package/dist/marketplace-install/env-var-fields.d.ts.map +1 -0
  123. package/dist/marketplace-install/env-var-fields.js +22 -0
  124. package/dist/marketplace-install/env-var-fields.js.map +1 -0
  125. package/dist/marketplace-install/index.d.ts +18 -0
  126. package/dist/marketplace-install/index.d.ts.map +1 -0
  127. package/dist/marketplace-install/index.js +8 -0
  128. package/dist/marketplace-install/index.js.map +1 -0
  129. package/dist/marketplace-install/install-env.d.ts +21 -0
  130. package/dist/marketplace-install/install-env.d.ts.map +1 -0
  131. package/dist/marketplace-install/install-env.js +22 -0
  132. package/dist/marketplace-install/install-env.js.map +1 -0
  133. package/dist/marketplace-install/install-mount.d.ts +42 -0
  134. package/dist/marketplace-install/install-mount.d.ts.map +1 -0
  135. package/dist/marketplace-install/install-mount.js +62 -0
  136. package/dist/marketplace-install/install-mount.js.map +1 -0
  137. package/dist/marketplace-install/install-report.d.ts +14 -0
  138. package/dist/marketplace-install/install-report.d.ts.map +1 -0
  139. package/dist/marketplace-install/install-report.js +19 -0
  140. package/dist/marketplace-install/install-report.js.map +1 -0
  141. package/dist/marketplace-install/install-shell.d.ts +34 -0
  142. package/dist/marketplace-install/install-shell.d.ts.map +1 -0
  143. package/dist/marketplace-install/install-shell.js +32 -0
  144. package/dist/marketplace-install/install-shell.js.map +1 -0
  145. package/dist/marketplace-install/mount-install.d.ts +41 -0
  146. package/dist/marketplace-install/mount-install.d.ts.map +1 -0
  147. package/dist/marketplace-install/mount-install.js +90 -0
  148. package/dist/marketplace-install/mount-install.js.map +1 -0
  149. package/dist/marketplace-install/plugin-detail.d.ts +17 -0
  150. package/dist/marketplace-install/plugin-detail.d.ts.map +1 -0
  151. package/dist/marketplace-install/plugin-detail.js +41 -0
  152. package/dist/marketplace-install/plugin-detail.js.map +1 -0
  153. package/dist/marketplace-install/plugin-install.d.ts +27 -0
  154. package/dist/marketplace-install/plugin-install.d.ts.map +1 -0
  155. package/dist/marketplace-install/plugin-install.js +89 -0
  156. package/dist/marketplace-install/plugin-install.js.map +1 -0
  157. package/dist/marketplace-install/route-items.d.ts +14 -0
  158. package/dist/marketplace-install/route-items.d.ts.map +1 -0
  159. package/dist/marketplace-install/route-items.js +11 -0
  160. package/dist/marketplace-install/route-items.js.map +1 -0
  161. package/dist/marketplace-install/use-debounced-value.d.ts +6 -0
  162. package/dist/marketplace-install/use-debounced-value.d.ts.map +1 -0
  163. package/dist/marketplace-install/use-debounced-value.js +18 -0
  164. package/dist/marketplace-install/use-debounced-value.js.map +1 -0
  165. package/dist/marketplace-install/use-install-action.d.ts +26 -0
  166. package/dist/marketplace-install/use-install-action.d.ts.map +1 -0
  167. package/dist/marketplace-install/use-install-action.js +35 -0
  168. package/dist/marketplace-install/use-install-action.js.map +1 -0
  169. package/dist/marketplace-install/use-install-preview.d.ts +33 -0
  170. package/dist/marketplace-install/use-install-preview.d.ts.map +1 -0
  171. package/dist/marketplace-install/use-install-preview.js +44 -0
  172. package/dist/marketplace-install/use-install-preview.js.map +1 -0
  173. package/dist/marketplace-install/use-mount-install.d.ts +80 -0
  174. package/dist/marketplace-install/use-mount-install.d.ts.map +1 -0
  175. package/dist/marketplace-install/use-mount-install.js +75 -0
  176. package/dist/marketplace-install/use-mount-install.js.map +1 -0
  177. package/dist/navigation/index.d.ts +1 -1
  178. package/dist/navigation/index.d.ts.map +1 -1
  179. package/dist/navigation/index.js.map +1 -1
  180. package/dist/navigation/types.d.ts +23 -7
  181. package/dist/navigation/types.d.ts.map +1 -1
  182. package/dist/plugin/registry.d.ts +0 -8
  183. package/dist/plugin/registry.d.ts.map +1 -1
  184. package/dist/plugin/registry.js +25 -1
  185. package/dist/plugin/registry.js.map +1 -1
  186. package/dist/plugin/types.d.ts +36 -12
  187. package/dist/plugin/types.d.ts.map +1 -1
  188. package/dist/plugin/version.d.ts +6 -4
  189. package/dist/plugin/version.d.ts.map +1 -1
  190. package/dist/plugin/version.js +6 -4
  191. package/dist/plugin/version.js.map +1 -1
  192. package/dist/query-keys.d.ts +45 -4
  193. package/dist/query-keys.d.ts.map +1 -1
  194. package/dist/query-keys.js +52 -4
  195. package/dist/query-keys.js.map +1 -1
  196. package/dist/schema-form/json-schema-meta.d.ts +3 -0
  197. package/dist/schema-form/json-schema-meta.d.ts.map +1 -0
  198. package/dist/schema-form/json-schema-meta.js +3 -0
  199. package/dist/schema-form/json-schema-meta.js.map +1 -0
  200. package/dist/testing.d.ts +2 -0
  201. package/dist/testing.d.ts.map +1 -1
  202. package/dist/testing.js +2 -0
  203. package/dist/testing.js.map +1 -1
  204. package/dist/token-rules.d.ts +26 -0
  205. package/dist/token-rules.d.ts.map +1 -0
  206. package/dist/token-rules.js +64 -0
  207. package/dist/token-rules.js.map +1 -0
  208. package/package.json +2 -2
  209. package/src/components/action-menu.tsx +160 -0
  210. package/src/components/agent-picker.tsx +53 -0
  211. package/src/components/channel-picker.tsx +68 -0
  212. package/src/components/components.css +226 -8
  213. package/src/components/confirm-dialog.tsx +4 -1
  214. package/src/components/execution-key-picker.tsx +252 -36
  215. package/src/components/extension-combo-builder.tsx +61 -29
  216. package/src/components/extension-combo-parts.tsx +28 -38
  217. package/src/components/extension-config-editor.tsx +307 -0
  218. package/src/components/extension-grouping.ts +4 -73
  219. package/src/components/extension-picker.tsx +13 -13
  220. package/src/components/fleet-report.tsx +38 -4
  221. package/src/components/holder-kinds.ts +60 -0
  222. package/src/components/holder-list.tsx +163 -0
  223. package/src/components/icons/index.ts +0 -2
  224. package/src/components/icons/nav-map.ts +0 -4
  225. package/src/components/icons/nav-marks.tsx +0 -15
  226. package/src/components/named-list-picker.tsx +182 -0
  227. package/src/components/named-list-test-utils.tsx +47 -0
  228. package/src/components/recycle-roll.tsx +120 -0
  229. package/src/components/stepper.tsx +111 -0
  230. package/src/components/subject-section.tsx +10 -5
  231. package/src/components/tokens.css +31 -41
  232. package/src/components/tokens.ts +5 -13
  233. package/src/components/tool-picker-parts.tsx +17 -6
  234. package/src/components/tool-picker.tsx +215 -41
  235. package/src/components/version-history-panel.tsx +8 -0
  236. package/src/components/version-history-rows.tsx +14 -2
  237. package/src/errors.ts +10 -0
  238. package/src/hooks/useCapabilities.ts +59 -10
  239. package/src/index.ts +54 -14
  240. package/src/marketplace-install/env-var-fields.tsx +59 -0
  241. package/src/marketplace-install/index.ts +17 -0
  242. package/src/marketplace-install/install-env.ts +39 -0
  243. package/src/marketplace-install/install-mount.ts +96 -0
  244. package/src/marketplace-install/install-report.tsx +54 -0
  245. package/src/marketplace-install/install-shell.tsx +99 -0
  246. package/src/marketplace-install/mount-install.tsx +290 -0
  247. package/src/marketplace-install/plugin-detail.ts +60 -0
  248. package/src/marketplace-install/plugin-install.tsx +221 -0
  249. package/src/marketplace-install/route-items.ts +20 -0
  250. package/src/marketplace-install/use-debounced-value.ts +18 -0
  251. package/src/marketplace-install/use-install-action.ts +60 -0
  252. package/src/marketplace-install/use-install-preview.ts +74 -0
  253. package/src/marketplace-install/use-mount-install.ts +116 -0
  254. package/src/navigation/index.ts +1 -0
  255. package/src/navigation/types.ts +27 -7
  256. package/src/plugin/registry.ts +19 -1
  257. package/src/plugin/types.ts +40 -12
  258. package/src/plugin/version.ts +6 -4
  259. package/src/query-keys.ts +71 -4
  260. package/src/schema-form/json-schema-meta.ts +2 -0
  261. package/src/testing.ts +2 -0
  262. package/src/token-rules.ts +66 -0
@@ -27,14 +27,19 @@ export interface SubjectTargetOption {
27
27
  /** The message a partially-filled subject is refused with (all three fields or none). */
28
28
  export const SUBJECT_INCOMPLETE_MESSAGE = 'A subject needs a target, a kind, and a key.';
29
29
 
30
- /** Map the conversation routes read to the target select's options. */
30
+ /** Map the conversation routes read to the target select's options: one option per
31
+ * distinct target, in first-seen order, since several routes may share a target. */
31
32
  export function toSubjectTargetOptions(
32
33
  routes: readonly { readonly target_kind: string; readonly target_name: string }[],
33
34
  ): SubjectTargetOption[] {
34
- return routes.map((route) => ({
35
- value: `${route.target_kind}:${route.target_name}`,
36
- label: `${route.target_kind} · ${route.target_name}`,
37
- }));
35
+ const byValue = new Map<string, SubjectTargetOption>();
36
+ for (const route of routes) {
37
+ const value = `${route.target_kind}:${route.target_name}`;
38
+ if (!byValue.has(value)) {
39
+ byValue.set(value, { value, label: `${route.target_kind} · ${route.target_name}` });
40
+ }
41
+ }
42
+ return [...byValue.values()];
38
43
  }
39
44
 
40
45
  /**
@@ -147,11 +147,14 @@
147
147
  /* -- Semantic ----------------------------------------------------------
148
148
  The `-text` members are the ONLY ones legible as text, dots, or icons;
149
149
  the `-fill` members are for fills and charts and carry `on-fill` ink
150
- labels in both themes. */
151
- --tai-color-ok-text: #047857;
150
+ labels in both themes. A badge label is a `-text` member over its own
151
+ tint, and that tint can sit on a row that is tinted too (a hovered or
152
+ highlighted row, a diff row), so the values are set to clear 4.5:1 there:
153
+ `badge-contrast.test.tsx` computes every such stack in both themes. */
154
+ --tai-color-ok-text: #065f46;
152
155
  --tai-dark-color-ok-text: #34d399;
153
156
  --tai-color-err-text: #b91c1c;
154
- --tai-dark-color-err-text: #f87171;
157
+ --tai-dark-color-err-text: #fca5a5;
155
158
  --tai-color-warn-text: #92400e;
156
159
  --tai-dark-color-warn-text: #fbbf24;
157
160
  --tai-color-ok-fill: #10b981;
@@ -196,30 +199,22 @@
196
199
  --tai-color-syntax-number: var(--tai-color-accent);
197
200
  --tai-color-syntax-bool: var(--tai-color-warn-text);
198
201
 
199
- /* -- Legacy token slots -------------------------------------------------
200
- The names a plugin already styles against. `primary`/`success`/`warning`
201
- re-point at the members of the contract above that keep them legible.
202
-
203
- `danger` is the one destructive EMPHASIS, and it runs deeper than the
204
- accent on purpose: a destructive fill sharing the crimson brand ramp reads
205
- as the primary action, so this is its own deep red — clearly darker and
206
- less pink than the accent in BOTH themes, unlike the light-fill dark-ink
207
- convention the accent follows. Its label is white in both themes and its
208
- hover a shade deeper still, so a destructive button is instantly told apart
209
- from a primary one. `danger-text` is that white label on the danger FILL;
210
- `danger-surface` is the faint error-tint ground the label sits over
211
- elsewhere. */
212
- --tai-color-primary: var(--tai-color-accent);
213
- --tai-color-primary-text: var(--tai-color-on-accent);
214
- --tai-color-danger: #991b1b;
215
- --tai-dark-color-danger: #dc2626;
216
- --tai-color-danger-text: #ffffff;
217
- --tai-dark-color-danger-text: #ffffff;
218
- --tai-color-danger-hover: #7f1d1d;
219
- --tai-dark-color-danger-hover: #b91c1c;
220
- --tai-color-danger-surface: var(--tai-color-err-tint);
221
- --tai-color-success: var(--tai-color-ok-text);
222
- --tai-color-warning: var(--tai-color-warn-text);
202
+ /* -- Destructive emphasis ---------------------------------------------
203
+ A FILL for the destructive button only, never text: as text the dark
204
+ fill fails contrast on the dark grounds, so error text takes `err-text`.
205
+ It runs deeper than the accent on purpose: a destructive fill sharing the
206
+ crimson brand ramp reads as the primary action, so this is its own deep
207
+ red, clearly darker and less pink than the accent in BOTH themes, unlike
208
+ the light-fill dark-ink convention the accent follows. `on-destructive` is
209
+ the white label on that fill in both themes, and the hover is a shade
210
+ deeper still, so a destructive button is instantly told apart from a
211
+ primary one. */
212
+ --tai-color-destructive: #991b1b;
213
+ --tai-dark-color-destructive: #dc2626;
214
+ --tai-color-destructive-hover: #7f1d1d;
215
+ --tai-dark-color-destructive-hover: #b91c1c;
216
+ --tai-color-on-destructive: #ffffff;
217
+ --tai-dark-color-on-destructive: #ffffff;
223
218
 
224
219
  /* -- Spacing (n x 4; there is deliberately no space-7) ------------------ */
225
220
  --tai-space-1: 0.25rem; /* 4px */
@@ -272,11 +267,6 @@
272
267
  --tai-dark-shadow-overlay-color: rgb(0 0 0 / 0.6);
273
268
  --tai-shadow-lift: 0 12px 32px var(--tai-shadow-lift-color);
274
269
  --tai-shadow-overlay: 0 24px 48px var(--tai-shadow-overlay-color);
275
- /* Legacy slots, re-pointed at the new elevation pair. Both names are kept
276
- and both changed MEANING: they were resting shadows (`0 1px 2px` /
277
- `0 4px 12px`) and now alias the lift and overlay geometry. */
278
- --tai-shadow-sm: var(--tai-shadow-lift);
279
- --tai-shadow-md: var(--tai-shadow-overlay);
280
270
 
281
271
  /* -- Stacking ------------------------------------------------------------
282
272
  `popover` and `tooltip` sit ABOVE the modal layers on purpose. Both are
@@ -284,9 +274,9 @@
284
274
  be opened from whatever layer is on top — so a listbox or a tip raised
285
275
  from inside a dialog has to clear that dialog's scrim to be usable.
286
276
  `dropdown` is the rung reserved for an in-flow menu surface — one that
287
- stacks against the page rather than being portalled. Nothing in this repo
288
- is on it; it is part of the published scale so a plugin's own menu has a
289
- rung to sit on instead of inventing a number. */
277
+ stacks against the page rather than being portalled. `ActionMenu`'s list
278
+ sits on it, and it is part of the published scale so a plugin's own menu
279
+ has a rung to sit on instead of inventing a number. */
290
280
  --tai-z-sticky: 10;
291
281
  --tai-z-dropdown: 20;
292
282
  --tai-z-overlay: 30;
@@ -335,9 +325,9 @@
335
325
  --tai-color-scrim: var(--tai-dark-color-scrim);
336
326
  --tai-color-prose-link: var(--tai-dark-color-prose-link);
337
327
  --tai-color-prose-link-hover: var(--tai-dark-color-prose-link-hover);
338
- --tai-color-danger: var(--tai-dark-color-danger);
339
- --tai-color-danger-text: var(--tai-dark-color-danger-text);
340
- --tai-color-danger-hover: var(--tai-dark-color-danger-hover);
328
+ --tai-color-destructive: var(--tai-dark-color-destructive);
329
+ --tai-color-destructive-hover: var(--tai-dark-color-destructive-hover);
330
+ --tai-color-on-destructive: var(--tai-dark-color-on-destructive);
341
331
  --tai-shadow-lift-color: var(--tai-dark-shadow-lift-color);
342
332
  --tai-shadow-overlay-color: var(--tai-dark-shadow-overlay-color);
343
333
  }
@@ -382,9 +372,9 @@
382
372
  --tai-color-scrim: var(--tai-dark-color-scrim);
383
373
  --tai-color-prose-link: var(--tai-dark-color-prose-link);
384
374
  --tai-color-prose-link-hover: var(--tai-dark-color-prose-link-hover);
385
- --tai-color-danger: var(--tai-dark-color-danger);
386
- --tai-color-danger-text: var(--tai-dark-color-danger-text);
387
- --tai-color-danger-hover: var(--tai-dark-color-danger-hover);
375
+ --tai-color-destructive: var(--tai-dark-color-destructive);
376
+ --tai-color-destructive-hover: var(--tai-dark-color-destructive-hover);
377
+ --tai-color-on-destructive: var(--tai-dark-color-on-destructive);
388
378
  --tai-shadow-lift-color: var(--tai-dark-shadow-lift-color);
389
379
  --tai-shadow-overlay-color: var(--tai-dark-shadow-overlay-color);
390
380
  }
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * The documented CSS-variable token names — the Studio-plugin styling API. A
3
- * snapshot test pins this list so the contract can't drift silently. The
4
- * SDK compatibility version bumps if any of these are renamed/removed; the list
5
- * grows additively.
3
+ * snapshot test pins this list so the contract can't drift silently. A rename
4
+ * or a removal bumps the SDK compatibility version; a new name does not.
6
5
  *
7
6
  * The `--tai-dark-*` half of each themed pair is deliberately NOT here. Those
8
7
  * names are the theming mechanism's own storage — the two blocks in `tokens.css`
@@ -49,14 +48,9 @@ export const TOKEN_NAMES = [
49
48
  '--tai-color-syntax-string',
50
49
  '--tai-color-syntax-number',
51
50
  '--tai-color-syntax-bool',
52
- '--tai-color-primary',
53
- '--tai-color-primary-text',
54
- '--tai-color-danger',
55
- '--tai-color-danger-text',
56
- '--tai-color-danger-hover',
57
- '--tai-color-danger-surface',
58
- '--tai-color-success',
59
- '--tai-color-warning',
51
+ '--tai-color-destructive',
52
+ '--tai-color-destructive-hover',
53
+ '--tai-color-on-destructive',
60
54
  '--tai-space-1',
61
55
  '--tai-space-2',
62
56
  '--tai-space-3',
@@ -89,8 +83,6 @@ export const TOKEN_NAMES = [
89
83
  '--tai-shadow-overlay-color',
90
84
  '--tai-shadow-lift',
91
85
  '--tai-shadow-overlay',
92
- '--tai-shadow-sm',
93
- '--tai-shadow-md',
94
86
  '--tai-z-sticky',
95
87
  '--tai-z-dropdown',
96
88
  '--tai-z-overlay',
@@ -8,7 +8,7 @@ import type { ReactNode } from 'react';
8
8
  import { toolDisplayLabel } from '../hooks/useToolDisplayNames';
9
9
  import { Badge } from './badge';
10
10
  import { Field } from './field';
11
- import { Select, type SelectGroup } from './select';
11
+ import { Select, type SelectGroup, type SelectOption } from './select';
12
12
  import { Tooltip } from './tooltip';
13
13
 
14
14
  /** The shared informational note every badge surface carries: declared, never enforced. */
@@ -80,6 +80,9 @@ interface ToolSelectProps {
80
80
  readonly disabled: boolean | undefined;
81
81
  readonly displayNames: Readonly<Record<string, string>> | undefined;
82
82
  readonly agentToolNames: ReadonlySet<string> | undefined;
83
+ /** An option drawn ahead of the list's own (a stored value the list does not carry). */
84
+ readonly leading: SelectOption | undefined;
85
+ readonly ariaDescribedBy: string | undefined;
83
86
  }
84
87
 
85
88
  /** The tool select: grouped by tag when `tagsByTool` is present, else a flat list.
@@ -94,32 +97,40 @@ export function ToolSelect({
94
97
  disabled,
95
98
  displayNames,
96
99
  agentToolNames,
100
+ leading,
101
+ ariaDescribedBy,
97
102
  }: ToolSelectProps): ReactNode {
98
103
  if (tagsByTool !== undefined) {
104
+ const groups = buildGroups(filtered, tagsByTool, displayNames, agentToolNames);
99
105
  return (
100
106
  <Select
101
- groups={buildGroups(filtered, tagsByTool, displayNames, agentToolNames)}
107
+ groups={
108
+ leading === undefined ? groups : [{ label: leading.label, options: [leading] }, ...groups]
109
+ }
102
110
  value={value ?? ''}
103
111
  onValueChange={onChange}
104
112
  placeholder={placeholder}
105
113
  aria-label={ariaLabel}
114
+ aria-describedby={ariaDescribedBy}
106
115
  disabled={disabled}
107
116
  />
108
117
  );
109
118
  }
119
+ const options = filtered.map((name) => ({
120
+ value: name,
121
+ label: optionLabel(name, displayNames, agentToolNames),
122
+ }));
110
123
  return (
111
124
  <Select
112
125
  // Keep Radix controlled even when the caller's value is null: `''` matches no
113
126
  // item (placeholder still shows) and avoids the uncontrolled→controlled warning
114
127
  // a subsequent selection would otherwise trigger.
115
- options={filtered.map((name) => ({
116
- value: name,
117
- label: optionLabel(name, displayNames, agentToolNames),
118
- }))}
128
+ options={leading === undefined ? options : [leading, ...options]}
119
129
  value={value ?? ''}
120
130
  onValueChange={onChange}
121
131
  placeholder={placeholder}
122
132
  aria-label={ariaLabel}
133
+ aria-describedby={ariaDescribedBy}
123
134
  disabled={disabled}
124
135
  />
125
136
  );
@@ -1,16 +1,25 @@
1
1
  /**
2
- * `ToolPicker` — a controlled single-select over the available tool names, built
2
+ * `ToolPicker` — a controlled single-select over the tool names a host offers, built
3
3
  * on the DS `Select` so features share one picker instead of each re-implementing
4
4
  * a listbox (the same reason `SchemaForm` lives in the SDK). It is
5
- * PRESENTATIONAL: the caller fetches the tool names (GET /api/tools) and owns the
6
- * selected value; this component fetches nothing.
5
+ * PRESENTATIONAL: the host owns the tool list read and hands it in as `query`, and
6
+ * owns the selected value; this component fetches nothing.
7
7
  *
8
- * Four optional extensions, each inert unless supplied; the preset create flow
9
- * passes all four and the multi-tool picker every one but `agentToolNames`:
8
+ * The picker owns its `Field` (with `label`) and draws the read's states in it: pending
9
+ * (a disabled `Select` reading "Loading tools…"), error (`ErrorState` with Retry, or the
10
+ * host's `errorNote` line where the same failure is drawn with Retry elsewhere), refused
11
+ * (a 403 with `forbiddenNote` set), and loaded with nothing to offer (the host's
12
+ * `emptyNote`, then its `emptyAction`). The host's `error` shows, and the `Field` claims
13
+ * its control id, exactly while a `Select` renders.
14
+ *
15
+ * Optional extensions, each inert unless supplied:
10
16
  * - `excludeNames` removes names from the options (e.g. a preset cannot be
11
17
  * another preset's base, so the preset create flow excludes existing preset
12
18
  * names). The server stays the authority — a slipped-through name is rejected
13
19
  * loudly there — this is only a UI convenience.
20
+ * - `unlistedLabel` flags a stored value the loaded list does not carry: it is the
21
+ * first option, labelled `<value> (<unlistedLabel>)`, with a warning line under the
22
+ * control. It never blocks: the server judges the save.
14
23
  * - `tagsByTool` (from GET /api/tools/tags) turns on tag GROUPING + a tag FILTER:
15
24
  * the options are grouped under their tag and a filter control narrows them to
16
25
  * a chosen tag.
@@ -31,26 +40,60 @@
31
40
  * SAFETY: a tool name, a tag, a display name, AND a badge are server-supplied, so
32
41
  * every one renders as TEXT (React escapes it) — never an HTML sink. Pinned by a test.
33
42
  */
34
- import { useState } from 'react';
43
+ import { type ReactNode, useId, useState } from 'react';
35
44
 
45
+ import { errorMessage, isForbiddenError } from '../errors';
46
+ import { Badge } from './badge';
36
47
  import { Field } from './field';
48
+ import { ErrorState } from './primitives';
49
+ import type { SelectOption } from './select';
37
50
  import { ALL_TAGS, SelectedToolBadges, TagFilterField, ToolSelect } from './tool-picker-parts';
38
51
 
39
52
  export { BADGES_NOTE } from './tool-picker-parts';
40
53
 
41
- /** The trigger copy when the picker has no option to offer at all (after exclusions).
42
- * Shown only once the list has settled — a still-loading picker keeps its own
43
- * placeholder — so it never misreads a pending fetch as "nothing to pick". */
44
- const NO_TOOLS_AVAILABLE = 'No tools available';
54
+ const MUTED_LINE = { margin: 0, color: 'var(--tai-color-text-muted)' } as const;
55
+
56
+ /**
57
+ * The subset of a tool list read this picker reads: `data` is the names the host
58
+ * offers. A caller's TanStack `UseQueryResult<readonly string[]>` satisfies it
59
+ * structurally, so the SDK names no query library.
60
+ */
61
+ export interface ToolListQuery {
62
+ readonly data: readonly string[] | undefined;
63
+ readonly isPending: boolean;
64
+ readonly isError: boolean;
65
+ readonly isSuccess: boolean;
66
+ readonly error: unknown;
67
+ readonly refetch: () => void;
68
+ }
45
69
 
46
70
  export interface ToolPickerProps {
47
- readonly toolNames: readonly string[];
71
+ /** The tool list read the host owns, `data` projected to the names it offers. */
72
+ readonly query: ToolListQuery;
48
73
  readonly value: string | null;
49
74
  readonly onChange: (toolName: string) => void;
75
+ /** The empty state's sentence, drawn when the loaded list offers no option (after `excludeNames`). */
76
+ readonly emptyNote: string;
77
+ /** Drawn after `emptyNote` in the empty state (e.g. a link to where tools come from). */
78
+ readonly emptyAction?: ReactNode;
79
+ /**
80
+ * Drawn in place of the `ErrorState` and the control when the read fails, with no
81
+ * Retry: for a host that already draws the same read's failure with Retry elsewhere.
82
+ */
83
+ readonly errorNote?: string;
84
+ /** Drawn on a 403 of the read in place of the error state (the no-grant state). */
85
+ readonly forbiddenNote?: string;
86
+ /** Flags a stored value the loaded list does not carry (see the module docs). */
87
+ readonly unlistedLabel?: string;
50
88
  readonly disabled?: boolean;
89
+ /** Shown once the list is loaded; a pending read reads "Loading tools…". */
51
90
  readonly placeholder?: string;
52
91
  readonly idPrefix?: string;
53
92
  readonly label?: string;
93
+ /** Handed to the picker's `Field` (with `label`). */
94
+ readonly description?: string;
95
+ /** The host's field error, shown under the control while a `Select` renders (with `label`). */
96
+ readonly error?: string;
54
97
  /**
55
98
  * Accessible name for the tool listbox when `label` renders none and no
56
99
  * enclosing `Field` names it. `label` wins where both are given: it names the
@@ -79,14 +122,112 @@ export interface ToolPickerProps {
79
122
  readonly badgesByTool?: Readonly<Record<string, readonly string[]>>;
80
123
  }
81
124
 
125
+ /** Which of the picker's states the read and the stored value put it in. */
126
+ type PickerPhase = 'refused' | 'error' | 'unlisted' | 'empty' | 'select';
127
+
128
+ interface PickerState {
129
+ readonly phase: PickerPhase;
130
+ readonly available: readonly string[];
131
+ /** The stored value, or `null` for none. */
132
+ readonly stored: string | null;
133
+ readonly pending: boolean;
134
+ /** The loaded list lacks the stored value and an `unlistedLabel` asks for the flag. */
135
+ readonly unlisted: boolean;
136
+ readonly empty: boolean;
137
+ }
138
+
139
+ function phaseOf(
140
+ refused: boolean,
141
+ failed: boolean,
142
+ unlisted: boolean,
143
+ empty: boolean,
144
+ ): PickerPhase {
145
+ if (refused) return 'refused';
146
+ if (failed) return 'error';
147
+ if (unlisted) return 'unlisted';
148
+ return empty ? 'empty' : 'select';
149
+ }
150
+
151
+ function pickerState(
152
+ query: ToolListQuery,
153
+ value: string | null,
154
+ excludeNames: readonly string[] | undefined,
155
+ forbiddenNote: string | undefined,
156
+ unlistedLabel: string | undefined,
157
+ ): PickerState {
158
+ const excluded = new Set(excludeNames ?? []);
159
+ const available = (query.data ?? []).filter((name) => !excluded.has(name));
160
+ const stored = value !== null && value !== '' ? value : null;
161
+ const loaded = query.isSuccess && !query.isError;
162
+ const unlisted =
163
+ unlistedLabel !== undefined && loaded && stored !== null && !available.includes(stored);
164
+ const empty = loaded && available.length === 0;
165
+ const refused = forbiddenNote !== undefined && query.isError && isForbiddenError(query.error);
166
+ const phase = phaseOf(refused, query.isError, unlisted, empty);
167
+ return { phase, available, stored, pending: !loaded && !query.isError, unlisted, empty };
168
+ }
169
+
170
+ /** The distinct tags across `names`, sorted, feeding the filter control. */
171
+ function tagsAcross(
172
+ names: readonly string[],
173
+ tagsByTool: Readonly<Record<string, readonly string[]>>,
174
+ ): string[] {
175
+ return [...new Set(names.flatMap((name) => tagsByTool[name] ?? []))].sort((a, b) =>
176
+ a.localeCompare(b),
177
+ );
178
+ }
179
+
180
+ /** The tag filter's choices, and the available names the chosen tag keeps. */
181
+ function tagView(
182
+ state: PickerState,
183
+ tagsByTool: Readonly<Record<string, readonly string[]>> | undefined,
184
+ tagFilter: string,
185
+ ): { readonly allTags: readonly string[]; readonly filtered: readonly string[] } {
186
+ if (tagsByTool === undefined) return { allTags: [], filtered: state.available };
187
+ const listed = !state.pending && state.phase !== 'error' && state.phase !== 'refused';
188
+ return {
189
+ allTags: listed ? tagsAcross(state.available, tagsByTool) : [],
190
+ filtered:
191
+ tagFilter === ALL_TAGS
192
+ ? state.available
193
+ : state.available.filter((name) => (tagsByTool[name] ?? []).includes(tagFilter)),
194
+ };
195
+ }
196
+
197
+ function MutedLine({ text }: { readonly text: string }): ReactNode {
198
+ return (
199
+ <p role="status" style={MUTED_LINE}>
200
+ {text}
201
+ </p>
202
+ );
203
+ }
204
+
205
+ function ReadError({ query }: { readonly query: ToolListQuery }): ReactNode {
206
+ return (
207
+ <ErrorState
208
+ message={errorMessage(query.error)}
209
+ onRetry={() => {
210
+ query.refetch();
211
+ }}
212
+ />
213
+ );
214
+ }
215
+
82
216
  export function ToolPicker({
83
- toolNames,
217
+ query,
84
218
  value,
85
219
  onChange,
220
+ emptyNote,
221
+ emptyAction,
222
+ errorNote,
223
+ forbiddenNote,
224
+ unlistedLabel,
86
225
  disabled,
87
226
  placeholder = 'Select a tool…',
88
227
  idPrefix = 'tool-picker',
89
228
  label,
229
+ description,
230
+ error,
90
231
  'aria-label': ariaLabel,
91
232
  excludeNames,
92
233
  tagsByTool,
@@ -95,47 +236,69 @@ export function ToolPicker({
95
236
  badgesByTool,
96
237
  }: ToolPickerProps) {
97
238
  const [tagFilter, setTagFilter] = useState<string>(ALL_TAGS);
239
+ const unlistedId = useId();
240
+ const state = pickerState(query, value, excludeNames, forbiddenNote, unlistedLabel);
241
+ const { phase, stored } = state;
98
242
 
99
- const excluded = new Set(excludeNames ?? []);
100
- const available = toolNames.filter((name) => !excluded.has(name));
101
-
102
- // A settled, empty option set (not a load in progress) is a "nothing to pick"
103
- // state: the trigger reads the empty copy and cannot open onto an empty popup. The
104
- // caller's `disabled` still marks a loading picker, which keeps its own placeholder.
105
- const noneAvailable = available.length === 0;
106
- const pickerDisabled = disabled === true || noneAvailable;
107
- const pickerPlaceholder = noneAvailable && disabled !== true ? NO_TOOLS_AVAILABLE : placeholder;
108
-
109
- // Distinct tags across the available tools, sorted, feeding the filter control.
110
- const allTags =
111
- tagsByTool === undefined
112
- ? []
113
- : [...new Set(available.flatMap((name) => tagsByTool[name] ?? []))].sort((a, b) =>
114
- a.localeCompare(b),
115
- );
116
-
117
- const filtered =
118
- tagsByTool !== undefined && tagFilter !== ALL_TAGS
119
- ? available.filter((name) => (tagsByTool[name] ?? []).includes(tagFilter))
120
- : available;
121
-
122
- const toolSelect = (
243
+ const { allTags, filtered } = tagView(state, tagsByTool, tagFilter);
244
+
245
+ const select = (options: readonly string[], leading: SelectOption | undefined): ReactNode => (
123
246
  <ToolSelect
124
247
  tagsByTool={tagsByTool}
125
- filtered={filtered}
248
+ filtered={options}
126
249
  value={value}
127
250
  onChange={onChange}
128
- placeholder={pickerPlaceholder}
251
+ placeholder={state.pending ? 'Loading tools…' : placeholder}
129
252
  ariaLabel={ariaLabel}
130
- disabled={pickerDisabled}
253
+ ariaDescribedBy={state.unlisted ? unlistedId : undefined}
254
+ disabled={disabled === true || state.pending}
131
255
  displayNames={displayNames}
132
256
  agentToolNames={agentToolNames}
257
+ leading={leading}
133
258
  />
134
259
  );
260
+ const emptyState = (
261
+ <>
262
+ <MutedLine text={emptyNote} />
263
+ {emptyAction}
264
+ </>
265
+ );
266
+
267
+ // Only a rendered `Select` claims the field's control id; with none the host's
268
+ // error is hidden and the note in its place says why the field is empty.
269
+ const selectRenders =
270
+ phase === 'unlisted' || phase === 'select' || (phase === 'refused' && stored !== null);
271
+ const contentByPhase: Record<PickerPhase, () => ReactNode> = {
272
+ // A stored value stays as the sole, unflagged option: without the list the form claims nothing about it.
273
+ refused: () => (
274
+ <>
275
+ {stored !== null ? select([], { value: stored, label: stored }) : null}
276
+ <MutedLine text={forbiddenNote ?? ''} />
277
+ </>
278
+ ),
279
+ error: () =>
280
+ errorNote !== undefined ? <MutedLine text={errorNote} /> : <ReadError query={query} />,
281
+ unlisted: () => (
282
+ <>
283
+ {select(filtered, {
284
+ value: stored ?? '',
285
+ label: `${stored ?? ''} (${unlistedLabel ?? ''})`,
286
+ })}
287
+ {state.empty ? emptyState : null}
288
+ <p id={unlistedId} className="tai-field-hint" style={{ margin: 0 }}>
289
+ <Badge variant="warning">{unlistedLabel}</Badge> {stored} is {unlistedLabel}. Pick
290
+ another, or saving is refused.
291
+ </p>
292
+ </>
293
+ ),
294
+ empty: () => emptyState,
295
+ select: () => select(filtered, undefined),
296
+ };
297
+ const content = contentByPhase[phase]();
135
298
 
136
299
  return (
137
300
  <div data-testid={idPrefix} className="tai-stack tai-stack-2">
138
- {tagsByTool !== undefined && allTags.length > 0 ? (
301
+ {allTags.length > 0 ? (
139
302
  <TagFilterField
140
303
  allTags={allTags}
141
304
  tagFilter={tagFilter}
@@ -143,7 +306,18 @@ export function ToolPicker({
143
306
  disabled={disabled}
144
307
  />
145
308
  ) : null}
146
- {label !== undefined ? <Field label={label}>{toolSelect}</Field> : toolSelect}
309
+ {label !== undefined ? (
310
+ <Field
311
+ label={label}
312
+ description={description}
313
+ error={selectRenders ? error : undefined}
314
+ group={!selectRenders}
315
+ >
316
+ {content}
317
+ </Field>
318
+ ) : (
319
+ content
320
+ )}
147
321
  <SelectedToolBadges value={value} badgesByTool={badgesByTool} idPrefix={idPrefix} />
148
322
  </div>
149
323
  );
@@ -82,6 +82,12 @@ export interface VersionHistoryPanelProps {
82
82
  * state. Omitted ⇒ no edit affordance (tags stay read-only).
83
83
  */
84
84
  readonly onEditTags?: (version: number, tags: string[]) => Promise<void>;
85
+ /**
86
+ * A version to point the reader at (a deep link names it): its row is marked
87
+ * `aria-current="true"` on the accent tint and scrolled into view. Omitted ⇒ no
88
+ * row is marked.
89
+ */
90
+ readonly highlightVersion?: number;
85
91
  }
86
92
 
87
93
  /** The initially-selected version: the current one, else the first listed. */
@@ -168,6 +174,7 @@ export function VersionHistoryPanel({
168
174
  readOnly = false,
169
175
  rollbackConfirmDescription,
170
176
  onEditTags,
177
+ highlightVersion,
171
178
  }: VersionHistoryPanelProps): ReactNode {
172
179
  const [selected, setSelected] = useState<number | null>(() => initialSelection(versions));
173
180
  const [confirming, setConfirming] = useState<number | null>(null);
@@ -246,6 +253,7 @@ export function VersionHistoryPanel({
246
253
  compareArmed={compareFrom === entry.version}
247
254
  readOnly={readOnly}
248
255
  canEditTags={onEditTags !== undefined}
256
+ highlighted={entry.version === highlightVersion}
249
257
  onView={view}
250
258
  onCompare={compare}
251
259
  onEditTags={setEditingTags}
@@ -4,7 +4,7 @@
4
4
  * the two-version compare dialog (`CompareDialog`), and the rollback confirm
5
5
  * dialog (`RollbackConfirmDialog`). All generic and kind-agnostic.
6
6
  */
7
- import type { ReactNode } from 'react';
7
+ import { type ReactNode, useEffect, useRef } from 'react';
8
8
 
9
9
  import { Dialog } from './dialog';
10
10
  import { CheckCircleIcon, PendingIcon } from './icons';
@@ -25,6 +25,8 @@ interface VersionRowProps {
25
25
  readonly readOnly: boolean;
26
26
  /** An edit-tags affordance is available (the consumer supplied `onEditTags`). */
27
27
  readonly canEditTags: boolean;
28
+ /** The row a deep link points at: marked current on the accent tint, scrolled to. */
29
+ readonly highlighted: boolean;
28
30
  readonly onView: (version: number) => void;
29
31
  readonly onCompare: (version: number) => void;
30
32
  readonly onEditTags: (version: number) => void;
@@ -40,13 +42,23 @@ export function VersionRow({
40
42
  compareArmed,
41
43
  readOnly,
42
44
  canEditTags,
45
+ highlighted,
43
46
  onView,
44
47
  onCompare,
45
48
  onEditTags,
46
49
  onRollback,
47
50
  }: VersionRowProps): ReactNode {
51
+ const rowRef = useRef<HTMLTableRowElement>(null);
52
+ useEffect(() => {
53
+ if (highlighted) rowRef.current?.scrollIntoView({ block: 'nearest' });
54
+ }, [highlighted]);
48
55
  return (
49
- <TR data-testid={`version-row-${String(entry.version)}`}>
56
+ <TR
57
+ ref={rowRef}
58
+ data-testid={`version-row-${String(entry.version)}`}
59
+ aria-current={highlighted ? 'true' : undefined}
60
+ style={highlighted ? { background: 'var(--tai-color-accent-tint)' } : undefined}
61
+ >
50
62
  <TD className="tai-table-id" numeric>
51
63
  {entry.version}
52
64
  </TD>
package/src/errors.ts CHANGED
@@ -10,3 +10,13 @@ export function errorMessage(error: unknown): string {
10
10
  if (typeof error === 'string') return error;
11
11
  return 'An unexpected error occurred.';
12
12
  }
13
+
14
+ /**
15
+ * True when `error` is a refusal for want of a grant: an object whose `status` is
16
+ * `403`. Duck-typed, so it reads an `ApiError` and any error shaped like one.
17
+ */
18
+ export function isForbiddenError(error: unknown): boolean {
19
+ return (
20
+ typeof error === 'object' && error !== null && (error as { status?: unknown }).status === 403
21
+ );
22
+ }