@tai42/studio-sdk 10.0.0 → 11.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 (72) hide show
  1. package/dist/components/components.css +18 -0
  2. package/dist/components/dirty-guard.d.ts +23 -10
  3. package/dist/components/dirty-guard.d.ts.map +1 -1
  4. package/dist/components/dirty-guard.js +22 -12
  5. package/dist/components/dirty-guard.js.map +1 -1
  6. package/dist/components/field.d.ts.map +1 -1
  7. package/dist/components/field.js +1 -1
  8. package/dist/components/field.js.map +1 -1
  9. package/dist/components/fleet-report.d.ts +12 -3
  10. package/dist/components/fleet-report.d.ts.map +1 -1
  11. package/dist/components/fleet-report.js +28 -9
  12. package/dist/components/fleet-report.js.map +1 -1
  13. package/dist/components/icons.d.ts +4 -0
  14. package/dist/components/icons.d.ts.map +1 -1
  15. package/dist/components/icons.js +5 -0
  16. package/dist/components/icons.js.map +1 -1
  17. package/dist/hooks/useApi.d.ts +348 -2
  18. package/dist/hooks/useApi.d.ts.map +1 -1
  19. package/dist/host.d.ts +1 -1
  20. package/dist/host.d.ts.map +1 -1
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/navigation/context.d.ts +6 -2
  26. package/dist/navigation/context.d.ts.map +1 -1
  27. package/dist/navigation/context.js +10 -2
  28. package/dist/navigation/context.js.map +1 -1
  29. package/dist/navigation/types.d.ts +1 -0
  30. package/dist/navigation/types.d.ts.map +1 -1
  31. package/dist/plugin/host-state.d.ts +16 -0
  32. package/dist/plugin/host-state.d.ts.map +1 -1
  33. package/dist/plugin/host-state.js +1 -0
  34. package/dist/plugin/host-state.js.map +1 -1
  35. package/dist/plugin/types.d.ts +17 -7
  36. package/dist/plugin/types.d.ts.map +1 -1
  37. package/dist/schema-form/SecretRefField.d.ts.map +1 -1
  38. package/dist/schema-form/SecretRefField.js +3 -2
  39. package/dist/schema-form/SecretRefField.js.map +1 -1
  40. package/dist/schema-form/default-value.d.ts +12 -0
  41. package/dist/schema-form/default-value.d.ts.map +1 -1
  42. package/dist/schema-form/default-value.js +34 -3
  43. package/dist/schema-form/default-value.js.map +1 -1
  44. package/dist/schema-form/field-node.d.ts +7 -0
  45. package/dist/schema-form/field-node.d.ts.map +1 -1
  46. package/dist/schema-form/field-node.js +3 -2
  47. package/dist/schema-form/field-node.js.map +1 -1
  48. package/dist/schema-form/object-fields.d.ts +3 -1
  49. package/dist/schema-form/object-fields.d.ts.map +1 -1
  50. package/dist/schema-form/object-fields.js +2 -2
  51. package/dist/schema-form/object-fields.js.map +1 -1
  52. package/dist/schema-form/union-field.d.ts +5 -2
  53. package/dist/schema-form/union-field.d.ts.map +1 -1
  54. package/dist/schema-form/union-field.js +68 -15
  55. package/dist/schema-form/union-field.js.map +1 -1
  56. package/package.json +2 -2
  57. package/src/components/components.css +18 -0
  58. package/src/components/dirty-guard.tsx +23 -13
  59. package/src/components/field.tsx +10 -5
  60. package/src/components/fleet-report.tsx +44 -13
  61. package/src/components/icons.tsx +19 -0
  62. package/src/host.ts +5 -1
  63. package/src/index.ts +3 -1
  64. package/src/navigation/context.tsx +16 -1
  65. package/src/navigation/types.ts +3 -0
  66. package/src/plugin/host-state.ts +18 -0
  67. package/src/plugin/types.ts +17 -7
  68. package/src/schema-form/SecretRefField.tsx +3 -2
  69. package/src/schema-form/default-value.ts +36 -3
  70. package/src/schema-form/field-node.tsx +21 -2
  71. package/src/schema-form/object-fields.tsx +4 -0
  72. package/src/schema-form/union-field.tsx +85 -19
@@ -1 +1 @@
1
- {"version":3,"file":"union-field.js","sourceRoot":"","sources":["../../src/schema-form/union-field.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"union-field.js","sourceRoot":"","sources":["../../src/schema-form/union-field.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tai42/studio-sdk",
3
- "version": "10.0.0",
3
+ "version": "11.0.0",
4
4
  "description": "The Studio plugin contract, shared UI components, and hooks that every feature builds on.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "tai42 <oss@tai42.ai>",
@@ -95,7 +95,7 @@
95
95
  "@radix-ui/react-select": "^2.3.2",
96
96
  "@radix-ui/react-tabs": "^1.1.16",
97
97
  "@radix-ui/react-tooltip": "^1.2.11",
98
- "@tai42/api-client": "10.0.0"
98
+ "@tai42/api-client": "11.0.0"
99
99
  },
100
100
  "scripts": {
101
101
  "build": "tsc -b tsconfig.build.json && node scripts/copy-assets.mjs",
@@ -984,6 +984,24 @@
984
984
  background-image: linear-gradient(var(--tai-color-accent-tint), var(--tai-color-accent-tint));
985
985
  }
986
986
 
987
+ /* The HOST-APPLIED provenance mark on plugin-contributed nav (the shell renders
988
+ it; a plugin cannot suppress or restyle it). A small muted glyph that links to
989
+ the plugin's marketplace row and brightens on hover/focus. It sits beside a nav
990
+ row (per-entry, in a core section) or beside a self-named section header. */
991
+ .tai-plugin-badge {
992
+ display: inline-flex;
993
+ align-items: center;
994
+ justify-content: center;
995
+ flex: none;
996
+ color: var(--tai-color-text-muted);
997
+ border-radius: var(--tai-radius-sm);
998
+ }
999
+
1000
+ .tai-plugin-badge:hover,
1001
+ .tai-plugin-badge:focus-visible {
1002
+ color: var(--tai-color-text);
1003
+ }
1004
+
987
1005
  /* Skip-to-content: off-screen until focused, so a keyboard or screen-reader
988
1006
  user can bypass the sidebar nav and jump straight to the page content
989
1007
  (WCAG 2.4.1). It is the first focusable element in the shell. */
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Unsaved-changes guard for tabbed editors. `Tabs` unmount inactive panels, so a
3
- * tab switch tears an editor down and drops its edits. One registry closes the gap:
4
- * editors report dirtiness via {@link useRegisterDirty}; {@link GuardedTabs} holds a
5
- * switch away from a dirty editor behind a confirm; the same registry arms
6
- * {@link useNavigationGuard}, so a route navigation confirms through the SAME dialog
7
- * and a full-page unload raises the native prompt. The dialog is one shared
2
+ * Unsaved-changes guard for editors that get torn down or navigated away from. One
3
+ * registry closes the gap: editors report dirtiness via {@link useRegisterDirty}; the
4
+ * enclosing {@link DirtyGuardBoundary} arms {@link useNavigationGuard} from that
5
+ * registry, so a route navigation confirms through a shared dialog and a full-page
6
+ * unload raises the native prompt. {@link GuardedTabs} adds a tab-switch confirm on
7
+ * top of the same boundary — `Tabs` unmount inactive panels, so a switch away from a
8
+ * dirty editor is held behind the confirm too. The dialog is one shared
8
9
  * {@link ConfirmDialog} driven by a pending-promise resolver, so the guard's
9
10
  * `Promise<boolean>` handler and the tab switch both settle against it.
10
11
  */
@@ -36,7 +37,16 @@ interface DirtyGuardValue {
36
37
 
37
38
  const DirtyGuardContext = createContext<DirtyGuardValue | null>(null);
38
39
 
39
- function DirtyGuardProvider({ children }: { readonly children: ReactNode }): ReactNode {
40
+ /**
41
+ * Boundary that guards navigation away from a region holding a dirty editor. Editors
42
+ * inside it arm the guard through {@link useRegisterDirty}; while any is dirty a route
43
+ * navigation (a route-token/plugin navigate, an {@link AppLink}, or a browser
44
+ * back/forward) confirms through the shared discard dialog, and a full-page unload
45
+ * raises the native prompt. Use this directly to guard a dirty editor that is a plain
46
+ * page section (no tabs); {@link GuardedTabs} wraps this same boundary to additionally
47
+ * guard tab switches.
48
+ */
49
+ export function DirtyGuardBoundary({ children }: { readonly children: ReactNode }): ReactNode {
40
50
  const dirtyIds = useRef<Set<string>>(new Set());
41
51
  // Mirror the registry's non-empty state into React so the navigation guard's
42
52
  // `when` re-arms as editors go dirty/clean.
@@ -96,9 +106,9 @@ function DirtyGuardProvider({ children }: { readonly children: ReactNode }): Rea
96
106
  }
97
107
 
98
108
  /**
99
- * Report an editor's dirty state to the enclosing {@link GuardedTabs}. A no-op
100
- * outside a provider (an editor rendered standalone, e.g. in isolation tests) so
101
- * the hook is always safe to call.
109
+ * Report an editor's dirty state to the enclosing {@link DirtyGuardBoundary} (which
110
+ * {@link GuardedTabs} also provides). A no-op outside a boundary (an editor rendered
111
+ * standalone, e.g. in isolation tests) so the hook is always safe to call.
102
112
  */
103
113
  export function useRegisterDirty(dirty: boolean): void {
104
114
  const ctx = useContext(DirtyGuardContext);
@@ -135,7 +145,7 @@ function GuardedTabsInner({
135
145
 
136
146
  /**
137
147
  * A drop-in replacement for {@link Tabs} that guards a switch away from a dirty
138
- * editor. Wraps the tabs in a {@link DirtyGuardProvider}, so an editor rendered in
148
+ * editor. Wraps the tabs in a {@link DirtyGuardBoundary}, so an editor rendered in
139
149
  * any panel can arm the guard through {@link useRegisterDirty}.
140
150
  */
141
151
  export function GuardedTabs({
@@ -146,8 +156,8 @@ export function GuardedTabs({
146
156
  readonly defaultValue?: string;
147
157
  }): ReactNode {
148
158
  return (
149
- <DirtyGuardProvider>
159
+ <DirtyGuardBoundary>
150
160
  <GuardedTabsInner items={items} defaultValue={defaultValue} />
151
- </DirtyGuardProvider>
161
+ </DirtyGuardBoundary>
152
162
  );
153
163
  }
@@ -155,6 +155,16 @@ export function Field({
155
155
  {label}
156
156
  </label>
157
157
  )}
158
+ {/* The description sits BETWEEN the label and the control, so it stays
159
+ adjacent to the label it glosses. After the control it drifts: a
160
+ group Field can wrap an arbitrarily tall surface (a whole schema
161
+ form), and a description below that surface reads as a footnote to
162
+ nothing. */}
163
+ {description !== undefined ? (
164
+ <p id={descriptionId} className="tai-field-hint" style={{ margin: 0 }}>
165
+ {description}
166
+ </p>
167
+ ) : null}
158
168
  {group ? (
159
169
  <div
160
170
  role="group"
@@ -167,11 +177,6 @@ export function Field({
167
177
  ) : (
168
178
  children
169
179
  )}
170
- {description !== undefined ? (
171
- <p id={descriptionId} className="tai-field-hint" style={{ margin: 0 }}>
172
- {description}
173
- </p>
174
- ) : null}
175
180
  {/* The icon carries the invalid state alongside the color, never the color
176
181
  alone — and it is the ERROR mark: a rejected value is a definite
177
182
  negative, not the warning the triangle states. */}
@@ -9,7 +9,10 @@
9
9
  * used by every config-mutation surface) reports a saved change that did not fully
10
10
  * propagate and points the operator at the System page to reload; `'reload'` (the
11
11
  * System page's own fleet-reload action) drops the "Change saved" framing — nothing
12
- * was saved, a reload was dispatched — and the self-referential System-page pointer.
12
+ * was saved, a reload was dispatched — and the self-referential System-page pointer;
13
+ * `'deregister'` and `'remove'` (detach fan-outs) likewise drop the "Change saved"
14
+ * framing and points the operator back at re-running the deregister, because a
15
+ * reload would re-attach the very server being detached — the wrong remediation.
13
16
  *
14
17
  * A `converged` summary (every worker applied, or a lone-worker local-only change)
15
18
  * renders NOTHING — the calling surface shows its own success. A `degraded` summary
@@ -73,26 +76,44 @@ const OUTCOME_STATUS: Record<FleetFailureOutcome, StatusTone> = {
73
76
  */
74
77
  export interface FleetReportProps {
75
78
  readonly summary: FleetReportSummary | null;
76
- /** Whose framing the copy takes: a saved config change, or a dispatched reload. */
77
- readonly action?: 'save' | 'reload';
79
+ /**
80
+ * Whose framing the copy takes: a saved config change (`'save'`), a dispatched
81
+ * reload (`'reload'`), a connector detach fan-out (`'deregister'`), or a live-registry
82
+ * tool removal (`'remove'`). All but `'save'` drop the "Change saved" framing;
83
+ * `'deregister'` and `'remove'` also point remediation back at re-running that
84
+ * same detach, since a reload would re-attach the detached server or tool.
85
+ */
86
+ readonly action?: 'save' | 'reload' | 'deregister' | 'remove';
78
87
  }
79
88
 
80
89
  export function FleetReport({ summary, action = 'save' }: FleetReportProps): ReactNode {
81
90
  if (summary === null || summary.status === 'converged') return null;
82
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';
95
+
83
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.';
84
107
  return (
85
108
  <div role="alert" className="tai-error-state tai-stack tai-stack-2">
86
109
  <strong className="tai-status tai-status-err">
87
110
  <XCircleIcon />
88
- {action === 'reload'
89
- ? 'The worker fleet was not reached'
90
- : 'Change saved, but the worker fleet was not reached'}
111
+ {savedFraming
112
+ ? 'Change saved, but the worker fleet was not reached'
113
+ : 'The worker fleet was not reached'}
91
114
  </strong>
92
115
  <p>
93
- {action === 'reload'
94
- ? 'The worker bus was unreachable, so other workers may still be running the old config. Re-run the reload once the bus is back.'
95
- : 'The worker bus was unreachable, so other workers may still be running the old config. Re-run the reload from the System page once the bus is back.'}
116
+ {`The worker bus was unreachable, so other workers may still be running the old config. ${unreachableRemediation}`}
96
117
  </p>
97
118
  {/* The bus error is a message, not a listing: it wraps rather than scrolls. */}
98
119
  {summary.error !== null ? (
@@ -111,17 +132,27 @@ export function FleetReport({ summary, action = 'save' }: FleetReportProps): Rea
111
132
  // worker is the commonest degraded fleet there is.
112
133
  const count = summary.failures.length;
113
134
  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';
114
145
  return (
115
146
  <div role="alert" className="tai-warn-state tai-stack tai-stack-2">
116
147
  <strong className="tai-status tai-status-warn">
117
148
  <AlertTriangleIcon />
118
- {action === 'reload'
119
- ? `${workers} did not converge`
120
- : `Change saved, but ${workers} did not converge`}
149
+ {savedFraming
150
+ ? `Change saved, but ${workers} did not converge`
151
+ : `${workers} did not converge`}
121
152
  </strong>
122
153
  <p>
123
154
  {`${count === 1 ? 'This worker' : 'These workers'} may still be running the old config — ` +
124
- (action === 'reload' ? 're-run the reload' : 'reload the fleet from the System page') +
155
+ remediation +
125
156
  ` to converge ${count === 1 ? 'it' : 'them'}.`}
126
157
  </p>
127
158
  <ul className="tai-stack tai-stack-2">
@@ -504,6 +504,24 @@ export const GridIcon: IconComponent = (props) => (
504
504
  </Icon>
505
505
  );
506
506
 
507
+ /** Served endpoints: a broadcast node radiating to the tools it serves. */
508
+ export const ServedEndpointsIcon: IconComponent = (props) => (
509
+ <Icon {...props}>
510
+ <circle cx="12" cy="12" r="2.5" />
511
+ <path d="M7.8 7.8a6 6 0 0 0 0 8.4" />
512
+ <path d="M16.2 7.8a6 6 0 0 1 0 8.4" />
513
+ <path d="M5 5a9 9 0 0 0 0 14" />
514
+ <path d="M19 5a9 9 0 0 1 0 14" />
515
+ </Icon>
516
+ );
517
+
518
+ /** Plugin: a puzzle piece — the host's provenance mark for plugin-contributed nav. */
519
+ export const PluginIcon: IconComponent = (props) => (
520
+ <Icon {...props}>
521
+ <path d="M10 4a2 2 0 0 1 4 0c0 .8.7 1.4 1.5 1.4H18a1 1 0 0 1 1 1v2.6c0 .8.6 1.5 1.4 1.5a2 2 0 0 1 0 4c-.8 0-1.4.7-1.4 1.5V19a1 1 0 0 1-1 1h-2.6c-.8 0-1.4-.6-1.4-1.4a2 2 0 0 0-4 0c0 .8-.6 1.4-1.4 1.4H6a1 1 0 0 1-1-1v-2.5C5 15.7 4.4 15 3.6 15a2 2 0 0 1 0-4c.8 0 1.4-.7 1.4-1.5V6a1 1 0 0 1 1-1h2.5C9.3 5 10 4.4 10 3.6z" />
522
+ </Icon>
523
+ );
524
+
507
525
  // -- Route mapping -----------------------------------------------------------
508
526
 
509
527
  /**
@@ -519,6 +537,7 @@ export const NAV_ICONS: Readonly<Record<Exclude<RouteToken, 'login'>, IconCompon
519
537
  extensions: ExtensionsIcon,
520
538
  templates: TemplatesIcon,
521
539
  connectors: ConnectorsIcon,
540
+ servedEndpoints: ServedEndpointsIcon,
522
541
  hooks: HooksIcon,
523
542
  storage: StorageIcon,
524
543
  scheduling: SchedulingIcon,
package/src/host.ts CHANGED
@@ -12,4 +12,8 @@ export {
12
12
  subscribePluginHost,
13
13
  usePluginContributions,
14
14
  } from './plugin/host-state';
15
- export type { PluginLoaderState, PluginContributionsSnapshot } from './plugin/host-state';
15
+ export type {
16
+ PluginLoaderState,
17
+ LoadedPlugin,
18
+ PluginContributionsSnapshot,
19
+ } from './plugin/host-state';
package/src/index.ts CHANGED
@@ -191,7 +191,7 @@ export { Dialog } from './components/dialog';
191
191
  export type { DialogProps } from './components/dialog';
192
192
  export { ConfirmDialog } from './components/confirm-dialog';
193
193
  export type { ConfirmDialogProps } from './components/confirm-dialog';
194
- export { GuardedTabs, useRegisterDirty } from './components/dirty-guard';
194
+ export { GuardedTabs, DirtyGuardBoundary, useRegisterDirty } from './components/dirty-guard';
195
195
  export { FormDialog } from './components/form-dialog';
196
196
  export type { FormDialogProps } from './components/form-dialog';
197
197
  export { Drawer } from './components/drawer';
@@ -318,6 +318,8 @@ export {
318
318
  ExtensionsIcon,
319
319
  TemplatesIcon,
320
320
  ConnectorsIcon,
321
+ ServedEndpointsIcon,
322
+ PluginIcon,
321
323
  HooksIcon,
322
324
  StorageIcon,
323
325
  SchedulingIcon,
@@ -8,6 +8,7 @@ import {
8
8
  useRef,
9
9
  useState,
10
10
  type AriaAttributes,
11
+ type CSSProperties,
11
12
  type MouseEvent,
12
13
  type ReactNode,
13
14
  } from 'react';
@@ -453,6 +454,10 @@ export interface AppLinkProps<T extends RouteToken> {
453
454
  className?: string;
454
455
  'aria-label'?: string;
455
456
  'aria-current'?: AriaAttributes['aria-current'];
457
+ /** Native tooltip text (also a sensible non-AT hover hint). */
458
+ title?: string;
459
+ /** Inline styles for the anchor. */
460
+ style?: CSSProperties;
456
461
  }
457
462
 
458
463
  /**
@@ -467,6 +472,8 @@ export function AppLink<T extends RouteToken>({
467
472
  className,
468
473
  'aria-label': ariaLabel,
469
474
  'aria-current': ariaCurrent,
475
+ title,
476
+ style,
470
477
  }: AppLinkProps<T>): ReactNode {
471
478
  const { navigate, resolvePath } = useNavigation();
472
479
  const href = resolvePath(to, search);
@@ -482,7 +489,15 @@ export function AppLink<T extends RouteToken>({
482
489
  );
483
490
  return createElement(
484
491
  'a',
485
- { href, className, 'aria-label': ariaLabel, 'aria-current': ariaCurrent, onClick },
492
+ {
493
+ href,
494
+ className,
495
+ 'aria-label': ariaLabel,
496
+ 'aria-current': ariaCurrent,
497
+ title,
498
+ style,
499
+ onClick,
500
+ },
486
501
  children,
487
502
  );
488
503
  }
@@ -41,6 +41,9 @@ export interface RouteSearchByToken {
41
41
  q?: string;
42
42
  };
43
43
  connectors: { connection?: string };
44
+ // The Served Endpoints surface (the promoted sub-MCP servers): a curated subset
45
+ // of tools served on a transport. No search parameters — a flat list + create form.
46
+ servedEndpoints: Record<string, never>;
44
47
  hooks: Record<string, never>;
45
48
  templates: { template?: string; q?: string };
46
49
  storage: { q?: string };
@@ -16,6 +16,17 @@ import { useSyncExternalStore } from 'react';
16
16
  import type { PluginContributions } from './types';
17
17
  import { getContributions } from './registry';
18
18
 
19
+ /**
20
+ * Identity of a successfully-loaded plugin, for host chrome that must attribute a
21
+ * contribution to its origin (e.g. the shell's provenance badge on plugin nav). The
22
+ * `name` is the same id the registry stamps on every contribution (`pluginId`); the
23
+ * `version` comes from the plugin's manifest.
24
+ */
25
+ export interface LoadedPlugin {
26
+ readonly name: string;
27
+ readonly version: string;
28
+ }
29
+
19
30
  /**
20
31
  * The plugin load pass's public state, mirrored for feature pages to read. It is
21
32
  * the shape the app shell's loader store already exposes: `status` gates when the
@@ -26,6 +37,12 @@ export interface PluginLoaderState {
26
37
  readonly status: 'idle' | 'loading' | 'ready';
27
38
  /** Names of plugins whose bundle imported and registered successfully. */
28
39
  readonly loaded: readonly string[];
40
+ /**
41
+ * Identity (name + version) of every successfully-loaded plugin, parallel to
42
+ * `loaded`. The host reads it to attribute a contribution to its plugin — e.g. the
43
+ * nav provenance badge's "Plugin: name version". Empty before any plugin loads.
44
+ */
45
+ readonly plugins: readonly LoadedPlugin[];
29
46
  /** Plugin name → loud error message (version mismatch / load failure). */
30
47
  readonly errors: Readonly<Record<string, string>>;
31
48
  /** A registry-listing failure that is NOT a 401 (surfaced loudly, never hidden). */
@@ -35,6 +52,7 @@ export interface PluginLoaderState {
35
52
  const INITIAL_HOST_STATE: PluginLoaderState = {
36
53
  status: 'idle',
37
54
  loaded: [],
55
+ plugins: [],
38
56
  errors: {},
39
57
  registryError: null,
40
58
  };
@@ -142,13 +142,15 @@ export interface RegisteredPage extends PageContribution {
142
142
 
143
143
  /**
144
144
  * The sidebar section a nav entry renders in. A named core section groups the
145
- * entry there, after that section's core rows; `'Plugins'`, an absent field, or
146
- * any unrecognised value falls to the Plugins section. The shell tolerates an
147
- * unknown value at runtime (a bundle newer than this host), so the field is a
148
- * placement hint, never a hard contract.
145
+ * entry there, after that section's core rows. An absent field or any value that
146
+ * is not a live core section renders the entry as a FIRST-CLASS section of its own,
147
+ * named for the contributing plugin (grouped with that plugin's other such entries),
148
+ * placed after the core sections. The shell tolerates an unknown value at runtime
149
+ * (a bundle newer than this host), falling back to that self-named section, so the
150
+ * field is a placement hint, never a hard contract.
149
151
  */
150
152
  export type NavEntrySection =
151
- 'Capabilities' | 'Integrations' | 'Activity' | 'Administration' | 'Plugins';
153
+ 'Capabilities' | 'Connections' | 'Triggers' | 'Activity' | 'Administration';
152
154
 
153
155
  export interface NavEntryContribution {
154
156
  /**
@@ -160,10 +162,18 @@ export interface NavEntryContribution {
160
162
  readonly title: string;
161
163
  /**
162
164
  * Optional target sidebar section (see {@link NavEntrySection}); absent ⇒ the
163
- * Plugins section. Additive — an older bundle omits it and stays on the same
164
- * plugin API version.
165
+ * plugin's own self-named section. Additive — an older bundle omits it and stays
166
+ * on the same plugin API version.
165
167
  */
166
168
  readonly section?: NavEntrySection;
169
+ /**
170
+ * Optional sort weight WITHIN the entry's section (whether a core section or the
171
+ * plugin's own self-named one): entries render in ascending `order`, and every
172
+ * entry that omits it sorts AFTER the ordered ones, in registration order. Two
173
+ * entries with the same `order` keep registration order (a stable sort). Additive —
174
+ * an older bundle omits it and stays on the same plugin API version.
175
+ */
176
+ readonly order?: number;
167
177
  /**
168
178
  * Optional icon rendered before the title. It must be a square inline SVG that
169
179
  * fills its box and draws with `currentColor`; the host constrains the slot,
@@ -3,8 +3,9 @@
3
3
  * EITHER a reference to an existing env key OR a freshly pasted secret. It knows
4
4
  * nothing about the `!ENV ${KEY}` marker syntax or any wire format: it emits a
5
5
  * discriminated {@link SecretRef} and the host maps that to whatever it stores
6
- * (the consumer that mounts this — the McpTab editor — writes an `!ENV ${KEY}`
7
- * marker for a `key` ref and runs the combined store-then-mark op for a `paste`).
6
+ * (the consumer that mounts this — the connectors page's McpServersSection —
7
+ * writes an `!ENV ${KEY}` marker for a `key` ref and runs the combined
8
+ * store-then-mark op for a `paste`).
8
9
  *
9
10
  * WRITE-ONLY for a pasted secret: the plaintext lives only in local editor state
10
11
  * until the user commits it, at which point it leaves the DOM entirely (held only
@@ -10,6 +10,10 @@
10
10
  * `default` — an optional object/array is NOT auto-materialized, so its own
11
11
  * required children can't raise spurious errors on a field the user never
12
12
  * opted into.
13
+ *
14
+ * `skeletonValueForSchema` is the sibling entry for programmatic reseeds (a
15
+ * union variant switch): same walk, but required scalars mint their type's
16
+ * empty value so the committed seed validates against its own schema.
13
17
  */
14
18
  import { classifySchema } from './classify';
15
19
  import type { JsonSchema } from './types';
@@ -30,6 +34,30 @@ function explicitDefault(
30
34
  * whole-tool-schema call).
31
35
  */
32
36
  export function defaultValueForSchema(schema: JsonSchema, root: JsonSchema = schema): unknown {
37
+ return seedValue(schema, root, false);
38
+ }
39
+
40
+ /**
41
+ * A SELF-CONSISTENT skeleton for a schema — the seed a variant switch (or any
42
+ * other programmatic reseed) commits as the field's new value. Where
43
+ * {@link defaultValueForSchema} deliberately leaves a required scalar absent so
44
+ * a fresh form flags it, a committed skeleton must validate against its own
45
+ * schema, so here EVERY required field with a known empty value is minted:
46
+ * required strings seed `''`, numbers `0`, booleans `false` — consistently with
47
+ * the `[]` / `{}` / nested-object seeds the default path already mints. Kinds
48
+ * with no empty value to name (enum, union, free-form JSON) stay absent either
49
+ * way: inventing a choice would be a lie the user never made.
50
+ */
51
+ export function skeletonValueForSchema(schema: JsonSchema, root: JsonSchema = schema): unknown {
52
+ return seedValue(schema, root, true);
53
+ }
54
+
55
+ /**
56
+ * The shared seeding walk. `mintScalars` distinguishes the two entry points
57
+ * above: `false` leaves value-less required scalars absent; `true` mints their
58
+ * type's empty value.
59
+ */
60
+ function seedValue(schema: JsonSchema, root: JsonSchema, mintScalars: boolean): unknown {
33
61
  const classified = classifySchema(schema, root);
34
62
  const explicit = explicitDefault(schema, classified.schema);
35
63
  if (explicit.present) return explicit.value;
@@ -42,7 +70,7 @@ export function defaultValueForSchema(schema: JsonSchema, root: JsonSchema = sch
42
70
  const result: Record<string, unknown> = {};
43
71
  for (const [name, propSchema] of model.properties) {
44
72
  if (model.required.has(name)) {
45
- const seeded = defaultValueForSchema(propSchema, root);
73
+ const seeded = seedValue(propSchema, root, mintScalars);
46
74
  if (seeded !== undefined) result[name] = seeded;
47
75
  continue;
48
76
  }
@@ -56,15 +84,20 @@ export function defaultValueForSchema(schema: JsonSchema, root: JsonSchema = sch
56
84
  return [];
57
85
  case 'record':
58
86
  return {};
59
- case 'enum':
60
87
  case 'string':
88
+ return mintScalars ? '' : undefined;
61
89
  case 'number':
90
+ return mintScalars ? 0 : undefined;
62
91
  case 'boolean':
92
+ return mintScalars ? false : undefined;
93
+ case 'enum':
63
94
  case 'union':
64
95
  case 'json':
65
96
  // A free-form JSON field starts ABSENT (its editor seeds an empty buffer),
66
97
  // like the other value-less kinds: an optional one never invents a value,
67
- // and a required one is left absent so validation flags it.
98
+ // and a required one is left absent so validation flags it. These kinds
99
+ // have no honest empty value even for a minted skeleton — an enum or a
100
+ // union would need a CHOICE invented on the user's behalf.
68
101
  return undefined;
69
102
  }
70
103
  }
@@ -30,6 +30,13 @@ interface FieldNodeProps {
30
30
  /** The label to show — a property key, array-item caption, or `undefined` at root. */
31
31
  readonly label: string | undefined;
32
32
  readonly required: boolean;
33
+ /**
34
+ * Append the design system's required marker (a trailing ` *`, the same
35
+ * convention host forms use on their own labels) to the heading when the
36
+ * field is required. Opt-in per face: the union variant surface sets it so
37
+ * the ACTIVE variant's obligations read at a glance.
38
+ */
39
+ readonly markRequired?: boolean;
33
40
  readonly errors: SchemaFormErrors | undefined;
34
41
  readonly idPrefix: string;
35
42
  }
@@ -40,9 +47,21 @@ function labelText(explicit: string | undefined, label: string | undefined): str
40
47
 
41
48
  /** A single schema node, dispatched on its classified model kind. */
42
49
  export function FieldNode(props: FieldNodeProps): ReactNode {
43
- const { schema, root, value, onChange, path, label, required, errors, idPrefix } = props;
50
+ const {
51
+ schema,
52
+ root,
53
+ value,
54
+ onChange,
55
+ path,
56
+ label,
57
+ required,
58
+ markRequired = false,
59
+ errors,
60
+ idPrefix,
61
+ } = props;
44
62
  const classified = classifySchema(schema, root);
45
- const heading = labelText(classified.title, label);
63
+ const base = labelText(classified.title, label);
64
+ const heading = markRequired && required ? `${base} *` : base;
46
65
  const description = classified.description;
47
66
  const error = errors?.[path];
48
67
  const model = classified.model;
@@ -23,6 +23,7 @@ export function ObjectFields({
23
23
  errors,
24
24
  idPrefix,
25
25
  skip,
26
+ markRequired = false,
26
27
  }: {
27
28
  properties: readonly (readonly [string, JsonSchema])[];
28
29
  requiredKeys: ReadonlySet<string>;
@@ -33,6 +34,8 @@ export function ObjectFields({
33
34
  errors: SchemaFormErrors | undefined;
34
35
  idPrefix: string;
35
36
  skip: string | undefined;
37
+ /** Mark each required property's label with the DS required marker. */
38
+ markRequired?: boolean;
36
39
  }): ReactNode {
37
40
  const obj = isPlainObject(value) ? value : {};
38
41
  return (
@@ -52,6 +55,7 @@ export function ObjectFields({
52
55
  path={childPath}
53
56
  label={name}
54
57
  required={requiredKeys.has(name)}
58
+ markRequired={markRequired}
55
59
  errors={errors}
56
60
  idPrefix={idPrefix}
57
61
  />