@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.
- package/dist/components/components.css +18 -0
- package/dist/components/dirty-guard.d.ts +23 -10
- package/dist/components/dirty-guard.d.ts.map +1 -1
- package/dist/components/dirty-guard.js +22 -12
- package/dist/components/dirty-guard.js.map +1 -1
- package/dist/components/field.d.ts.map +1 -1
- package/dist/components/field.js +1 -1
- package/dist/components/field.js.map +1 -1
- package/dist/components/fleet-report.d.ts +12 -3
- package/dist/components/fleet-report.d.ts.map +1 -1
- package/dist/components/fleet-report.js +28 -9
- package/dist/components/fleet-report.js.map +1 -1
- package/dist/components/icons.d.ts +4 -0
- package/dist/components/icons.d.ts.map +1 -1
- package/dist/components/icons.js +5 -0
- package/dist/components/icons.js.map +1 -1
- package/dist/hooks/useApi.d.ts +348 -2
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/host.d.ts +1 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/navigation/context.d.ts +6 -2
- package/dist/navigation/context.d.ts.map +1 -1
- package/dist/navigation/context.js +10 -2
- package/dist/navigation/context.js.map +1 -1
- package/dist/navigation/types.d.ts +1 -0
- package/dist/navigation/types.d.ts.map +1 -1
- package/dist/plugin/host-state.d.ts +16 -0
- package/dist/plugin/host-state.d.ts.map +1 -1
- package/dist/plugin/host-state.js +1 -0
- package/dist/plugin/host-state.js.map +1 -1
- package/dist/plugin/types.d.ts +17 -7
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/schema-form/SecretRefField.d.ts.map +1 -1
- package/dist/schema-form/SecretRefField.js +3 -2
- package/dist/schema-form/SecretRefField.js.map +1 -1
- package/dist/schema-form/default-value.d.ts +12 -0
- package/dist/schema-form/default-value.d.ts.map +1 -1
- package/dist/schema-form/default-value.js +34 -3
- package/dist/schema-form/default-value.js.map +1 -1
- package/dist/schema-form/field-node.d.ts +7 -0
- package/dist/schema-form/field-node.d.ts.map +1 -1
- package/dist/schema-form/field-node.js +3 -2
- package/dist/schema-form/field-node.js.map +1 -1
- package/dist/schema-form/object-fields.d.ts +3 -1
- package/dist/schema-form/object-fields.d.ts.map +1 -1
- package/dist/schema-form/object-fields.js +2 -2
- package/dist/schema-form/object-fields.js.map +1 -1
- package/dist/schema-form/union-field.d.ts +5 -2
- package/dist/schema-form/union-field.d.ts.map +1 -1
- package/dist/schema-form/union-field.js +68 -15
- package/dist/schema-form/union-field.js.map +1 -1
- package/package.json +2 -2
- package/src/components/components.css +18 -0
- package/src/components/dirty-guard.tsx +23 -13
- package/src/components/field.tsx +10 -5
- package/src/components/fleet-report.tsx +44 -13
- package/src/components/icons.tsx +19 -0
- package/src/host.ts +5 -1
- package/src/index.ts +3 -1
- package/src/navigation/context.tsx +16 -1
- package/src/navigation/types.ts +3 -0
- package/src/plugin/host-state.ts +18 -0
- package/src/plugin/types.ts +17 -7
- package/src/schema-form/SecretRefField.tsx +3 -2
- package/src/schema-form/default-value.ts +36 -3
- package/src/schema-form/field-node.tsx +21 -2
- package/src/schema-form/object-fields.tsx +4 -0
- 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":";
|
|
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": "
|
|
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": "
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* {@link
|
|
7
|
-
*
|
|
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
|
-
|
|
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
|
|
100
|
-
* outside a
|
|
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
|
|
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
|
-
<
|
|
159
|
+
<DirtyGuardBoundary>
|
|
150
160
|
<GuardedTabsInner items={items} defaultValue={defaultValue} />
|
|
151
|
-
</
|
|
161
|
+
</DirtyGuardBoundary>
|
|
152
162
|
);
|
|
153
163
|
}
|
package/src/components/field.tsx
CHANGED
|
@@ -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
|
-
/**
|
|
77
|
-
|
|
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
|
-
{
|
|
89
|
-
? '
|
|
90
|
-
: '
|
|
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
|
-
{
|
|
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
|
-
{
|
|
119
|
-
?
|
|
120
|
-
:
|
|
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
|
-
|
|
155
|
+
remediation +
|
|
125
156
|
` to converge ${count === 1 ? 'it' : 'them'}.`}
|
|
126
157
|
</p>
|
|
127
158
|
<ul className="tai-stack tai-stack-2">
|
package/src/components/icons.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
{
|
|
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
|
}
|
package/src/navigation/types.ts
CHANGED
|
@@ -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 };
|
package/src/plugin/host-state.ts
CHANGED
|
@@ -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
|
};
|
package/src/plugin/types.ts
CHANGED
|
@@ -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
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
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' | '
|
|
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
|
-
*
|
|
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
|
|
7
|
-
* marker for a `key` ref and runs the combined
|
|
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 =
|
|
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 {
|
|
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
|
|
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
|
/>
|