@tai42/studio-sdk 19.3.0 → 21.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 +7 -0
- package/dist/components/execution-key-picker.js.map +1 -1
- package/dist/components/extension-grouping.d.ts +8 -12
- package/dist/components/extension-grouping.d.ts.map +1 -1
- package/dist/components/extension-grouping.js +12 -15
- package/dist/components/extension-grouping.js.map +1 -1
- package/dist/components/extension-picker.d.ts +3 -4
- package/dist/components/extension-picker.d.ts.map +1 -1
- package/dist/components/extension-picker.js +10 -10
- package/dist/components/extension-picker.js.map +1 -1
- package/dist/components/fleet-report.d.ts +10 -5
- package/dist/components/fleet-report.d.ts.map +1 -1
- package/dist/components/fleet-report.js +20 -3
- package/dist/components/fleet-report.js.map +1 -1
- package/dist/components/icons/index.d.ts +1 -1
- package/dist/components/icons/index.d.ts.map +1 -1
- package/dist/components/icons/index.js +1 -1
- package/dist/components/icons/index.js.map +1 -1
- package/dist/components/icons/nav-map.d.ts.map +1 -1
- package/dist/components/icons/nav-map.js +2 -1
- package/dist/components/icons/nav-map.js.map +1 -1
- package/dist/components/icons/nav-marks.d.ts +2 -0
- package/dist/components/icons/nav-marks.d.ts.map +1 -1
- package/dist/components/icons/nav-marks.js +2 -0
- package/dist/components/icons/nav-marks.js.map +1 -1
- package/dist/components/json-tree-model.d.ts +19 -7
- package/dist/components/json-tree-model.d.ts.map +1 -1
- package/dist/components/json-tree-model.js +32 -8
- package/dist/components/json-tree-model.js.map +1 -1
- package/dist/components/json-tree.d.ts +10 -1
- package/dist/components/json-tree.d.ts.map +1 -1
- package/dist/components/json-tree.js +7 -4
- package/dist/components/json-tree.js.map +1 -1
- package/dist/components/select.d.ts +6 -0
- package/dist/components/select.d.ts.map +1 -1
- package/dist/components/select.js +3 -2
- package/dist/components/select.js.map +1 -1
- package/dist/components/served-media.d.ts +50 -4
- package/dist/components/served-media.d.ts.map +1 -1
- package/dist/components/served-media.js +69 -19
- package/dist/components/served-media.js.map +1 -1
- package/dist/env-markers.d.ts +25 -0
- package/dist/env-markers.d.ts.map +1 -0
- package/dist/env-markers.js +63 -0
- package/dist/env-markers.js.map +1 -0
- package/dist/hooks/interactions-stream.d.ts.map +1 -1
- package/dist/hooks/interactions-stream.js +9 -7
- package/dist/hooks/interactions-stream.js.map +1 -1
- package/dist/hooks/useApi.d.ts +2244 -862
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/hooks/useCapabilities.d.ts +5 -9
- package/dist/hooks/useCapabilities.d.ts.map +1 -1
- package/dist/hooks/useCapabilities.js +8 -33
- package/dist/hooks/useCapabilities.js.map +1 -1
- package/dist/host.d.ts +2 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +8 -0
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/navigation/types.d.ts +1 -0
- package/dist/navigation/types.d.ts.map +1 -1
- package/dist/plugin/coverage.d.ts +14 -0
- package/dist/plugin/coverage.d.ts.map +1 -0
- package/dist/plugin/coverage.js +14 -0
- package/dist/plugin/coverage.js.map +1 -0
- package/dist/plugin/registry.d.ts +11 -1
- package/dist/plugin/registry.d.ts.map +1 -1
- package/dist/plugin/registry.js +33 -4
- package/dist/plugin/registry.js.map +1 -1
- package/dist/plugin/types.d.ts +43 -1
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/state-binding/UpdateRow.d.ts +2 -2
- package/dist/state-binding/UpdateRow.js +1 -1
- package/dist/state-binding/UpdateRow.js.map +1 -1
- package/dist/state-binding/adapter.d.ts +12 -8
- package/dist/state-binding/adapter.d.ts.map +1 -1
- package/dist/state-binding/adapter.js +30 -13
- package/dist/state-binding/adapter.js.map +1 -1
- package/package.json +5 -4
- package/src/components/components.css +7 -0
- package/src/components/execution-key-picker.tsx +1 -1
- package/src/components/extension-grouping.ts +22 -22
- package/src/components/extension-picker.tsx +13 -14
- package/src/components/fleet-report.tsx +66 -7
- package/src/components/icons/index.ts +1 -0
- package/src/components/icons/nav-map.ts +2 -0
- package/src/components/icons/nav-marks.tsx +10 -0
- package/src/components/json-tree-model.ts +36 -8
- package/src/components/json-tree.tsx +24 -4
- package/src/components/select.tsx +11 -1
- package/src/components/served-media.tsx +150 -16
- package/src/env-markers.ts +70 -0
- package/src/hooks/interactions-stream.ts +9 -7
- package/src/hooks/useCapabilities.ts +8 -31
- package/src/host.ts +2 -0
- package/src/index.ts +17 -2
- package/src/navigation/types.ts +3 -0
- package/src/plugin/coverage.ts +23 -0
- package/src/plugin/registry.ts +58 -3
- package/src/plugin/types.ts +45 -1
- package/src/state-binding/README.md +10 -7
- package/src/state-binding/UpdateRow.tsx +3 -3
- package/src/state-binding/adapter.ts +31 -16
|
@@ -131,8 +131,8 @@ export function isFullProjection(projection: MeProjection): boolean {
|
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
133
|
* Whether a concrete path is `prefix` itself or nested beneath it at a PATH-SEGMENT
|
|
134
|
-
* boundary — the client mirror of the server's segment-aware coverage rule
|
|
135
|
-
*
|
|
134
|
+
* boundary — the client mirror of the server's segment-aware coverage rule, held to
|
|
135
|
+
* the platform's published path-coverage vectors by the shell's tests. Matching on a
|
|
136
136
|
* bare `startsWith` would let a sibling route like `/api/tools-schema` satisfy the
|
|
137
137
|
* `/api/tools` token; the boundary keeps them distinct.
|
|
138
138
|
*/
|
|
@@ -144,22 +144,18 @@ function pathUnderPrefix(path: string, prefix: string): boolean {
|
|
|
144
144
|
* Whether the projection reaches ANY route under one of `prefixes` — the
|
|
145
145
|
* declarative test behind nav filtering and plugin `requiredCapabilities`. A
|
|
146
146
|
* concrete `routes[].path` covers a prefix when it is that prefix or a
|
|
147
|
-
* segment-nested descendant of it; a dynamic `route_patterns[]
|
|
148
|
-
* prefix
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* over-showing is the bug. So any non-anchored or otherwise complex pattern
|
|
153
|
-
* (alternation, char class, bounded quantifier) is conservatively treated as NOT
|
|
154
|
-
* covering — its matched set is not a clean prefix, so we never infer reach from
|
|
155
|
-
* it.
|
|
147
|
+
* segment-nested descendant of it; a dynamic `route_patterns[]` entry covers a
|
|
148
|
+
* prefix through its served `open_prefix` — the literal the server rules every
|
|
149
|
+
* path starting with is reachable — when it overlaps the prefix at a segment
|
|
150
|
+
* boundary. A `null` open prefix (a pattern whose matched set is not a clean
|
|
151
|
+
* prefix) covers nothing by prefix: under-showing is safe, over-showing is the bug.
|
|
156
152
|
*/
|
|
157
153
|
export function coversAnyRoute(projection: MeProjection, prefixes: readonly string[]): boolean {
|
|
158
154
|
return prefixes.some(
|
|
159
155
|
(prefix) =>
|
|
160
156
|
projection.routes.some((route) => pathUnderPrefix(route.path, prefix)) ||
|
|
161
157
|
projection.route_patterns.some((entry) => {
|
|
162
|
-
const open =
|
|
158
|
+
const open = entry.open_prefix;
|
|
163
159
|
// The open literal L means "every path starting with L is reachable". That
|
|
164
160
|
// set reaches something under `prefix` when the prefix itself lies in it
|
|
165
161
|
// (`prefix.startsWith(open)`) or when the literal already dives under the
|
|
@@ -221,22 +217,3 @@ export function useCanWrite(path: string, method = 'POST'): boolean {
|
|
|
221
217
|
const { state } = useCapabilities();
|
|
222
218
|
return coversWrite(state, path, method);
|
|
223
219
|
}
|
|
224
|
-
|
|
225
|
-
const REGEX_METACHARACTERS = /[.*+?()[\]{}|\\^$]/;
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* The literal prefix of a simple anchored open-wildcard pattern (`^<literal>.*$`,
|
|
229
|
-
* the `$` optional), or `null` when the pattern is not exactly that shape. A
|
|
230
|
-
* returned literal `L` means "every path starting with `L` is reachable"; only
|
|
231
|
-
* that open-wildcard form yields a sound prefix, so a bare literal, an
|
|
232
|
-
* alternation, a char class, or any other construct declines (returns `null`).
|
|
233
|
-
*/
|
|
234
|
-
function patternOpenPrefix(pattern: string): string | null {
|
|
235
|
-
if (!pattern.startsWith('^')) return null;
|
|
236
|
-
let body = pattern.slice(1);
|
|
237
|
-
if (body.endsWith('$')) body = body.slice(0, -1);
|
|
238
|
-
if (!body.endsWith('.*')) return null;
|
|
239
|
-
body = body.slice(0, -2);
|
|
240
|
-
if (body.length === 0 || REGEX_METACHARACTERS.test(body)) return null;
|
|
241
|
-
return body;
|
|
242
|
-
}
|
package/src/host.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* carries no way to forge, wipe, or enumerate the registry. Only the host bundle
|
|
6
6
|
* imports this. See SECURITY.md for the trust boundary.
|
|
7
7
|
*/
|
|
8
|
+
export { contributionCovered } from './plugin/coverage';
|
|
8
9
|
export type {
|
|
9
10
|
LoadedPlugin,
|
|
10
11
|
PluginContributionsSnapshot,
|
|
@@ -16,4 +17,5 @@ export {
|
|
|
16
17
|
subscribePluginHost,
|
|
17
18
|
usePluginContributions,
|
|
18
19
|
} from './plugin/host-state';
|
|
20
|
+
export type { PluginLoadInputs } from './plugin/registry';
|
|
19
21
|
export { getContributions, loadPlugin } from './plugin/registry';
|
package/src/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ import './components/components.css';
|
|
|
41
41
|
|
|
42
42
|
// -- Plugin API --------------------------------------------------------------
|
|
43
43
|
export type {
|
|
44
|
+
ChannelConversationRoute,
|
|
44
45
|
NavEntryContribution,
|
|
45
46
|
NavEntrySection,
|
|
46
47
|
PageContribution,
|
|
@@ -51,8 +52,11 @@ export type {
|
|
|
51
52
|
PluginPageProps,
|
|
52
53
|
RegisteredNavEntry,
|
|
53
54
|
RegisteredPage,
|
|
55
|
+
RegisteredRoutePanel,
|
|
54
56
|
RegisteredSettingsTab,
|
|
55
57
|
RequiredCapabilities,
|
|
58
|
+
RoutePanelContribution,
|
|
59
|
+
RoutePanelProps,
|
|
56
60
|
SettingsTabContribution,
|
|
57
61
|
SettingsTabProps,
|
|
58
62
|
ToolPanelContribution,
|
|
@@ -60,6 +64,8 @@ export type {
|
|
|
60
64
|
} from './plugin/types';
|
|
61
65
|
export type { VersionGateResult } from './plugin/version';
|
|
62
66
|
export { checkPluginApiVersion, STUDIO_PLUGIN_API_VERSION } from './plugin/version';
|
|
67
|
+
/** What `useApi().runTool` resolves with: the run-tool door's answer, read by its `kind`. */
|
|
68
|
+
export type { RunAnswer } from '@tai42/api-client';
|
|
63
69
|
|
|
64
70
|
// -- Hooks -------------------------------------------------------------------
|
|
65
71
|
export type { ApiClient } from './hooks/useApi';
|
|
@@ -133,6 +139,8 @@ export {
|
|
|
133
139
|
|
|
134
140
|
// -- Utilities ---------------------------------------------------------------
|
|
135
141
|
export { downloadBlob } from './download';
|
|
142
|
+
export type { EnvRef } from './env-markers';
|
|
143
|
+
export { collectEnvRefs, ENV_MARKER_PREFIX, formatEnvMarker, parseEnvMarker } from './env-markers';
|
|
136
144
|
export { errorMessage } from './errors';
|
|
137
145
|
|
|
138
146
|
// -- Disabled-feature idiom (one helper + component, every consumer) ----------
|
|
@@ -292,11 +300,18 @@ export type { HeadingLevel, MarkdownBlock, MarkdownProps } from './components/ma
|
|
|
292
300
|
export { Markdown, parseMarkdown } from './components/markdown';
|
|
293
301
|
export type { OpenTargetOptions, OpenTargetProps } from './components/open-target';
|
|
294
302
|
export { openTargetProps } from './components/open-target';
|
|
295
|
-
export type {
|
|
303
|
+
export type {
|
|
304
|
+
MediaDocumentProps,
|
|
305
|
+
MediaImageProps,
|
|
306
|
+
MediaPlayerProps,
|
|
307
|
+
ServedMediaGrammar,
|
|
308
|
+
} from './components/served-media';
|
|
296
309
|
export {
|
|
297
310
|
isRenderableMediaSrc,
|
|
298
311
|
isServedMediaUrl,
|
|
312
|
+
MediaDocument,
|
|
299
313
|
MediaImage,
|
|
314
|
+
MediaPlayer,
|
|
300
315
|
resolveMediaSrc,
|
|
301
316
|
} from './components/served-media';
|
|
302
317
|
// The URL half of the link-safety pair: `isSafeHttpUrl` answers yes/no, this one
|
|
@@ -313,7 +328,6 @@ export {
|
|
|
313
328
|
groupByKind,
|
|
314
329
|
groupIntoFamilies,
|
|
315
330
|
kindVariant,
|
|
316
|
-
NON_STACKABLE_KIND,
|
|
317
331
|
} from './components/extension-grouping';
|
|
318
332
|
export type { ExtensionPickerProps } from './components/extension-picker';
|
|
319
333
|
export { ExtensionPicker } from './components/extension-picker';
|
|
@@ -369,6 +383,7 @@ export {
|
|
|
369
383
|
InteractionsIcon,
|
|
370
384
|
ManifestIcon,
|
|
371
385
|
MarketplaceIcon,
|
|
386
|
+
MembersIcon,
|
|
372
387
|
MenuIcon,
|
|
373
388
|
MinusIcon,
|
|
374
389
|
MonitorIcon,
|
package/src/navigation/types.ts
CHANGED
|
@@ -62,6 +62,9 @@ export interface RouteSearchByToken {
|
|
|
62
62
|
templates: { template?: string; q?: string };
|
|
63
63
|
storage: { q?: string };
|
|
64
64
|
manifest: Record<string, never>;
|
|
65
|
+
// The generic Members listing: people + outstanding invitations aggregated across
|
|
66
|
+
// every accounts provider. No search parameters — a flat read-only view.
|
|
67
|
+
members: Record<string, never>;
|
|
65
68
|
settings: Record<string, never>;
|
|
66
69
|
system: Record<string, never>;
|
|
67
70
|
scheduling: Record<string, never>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a plugin contribution is visible under a capability projection — the one
|
|
3
|
+
* evaluator every host surface applies to a contribution's optional
|
|
4
|
+
* `requiredCapabilities`. Host-only (`@tai42/studio-sdk/host`).
|
|
5
|
+
*/
|
|
6
|
+
import type { MeProjection } from '@tai42/api-client';
|
|
7
|
+
|
|
8
|
+
import { coversAnyRoute, isFullProjection } from '../hooks/useCapabilities';
|
|
9
|
+
import type { RequiredCapabilities } from './types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A full projection shows every contribution; an ABSENT requirement renders only for a
|
|
13
|
+
* full projection (safe-by-default); a declared requirement renders when the projection
|
|
14
|
+
* reaches at least one of its routes.
|
|
15
|
+
*/
|
|
16
|
+
export function contributionCovered(
|
|
17
|
+
projection: MeProjection,
|
|
18
|
+
required: RequiredCapabilities | undefined,
|
|
19
|
+
): boolean {
|
|
20
|
+
if (isFullProjection(projection)) return true;
|
|
21
|
+
if (required === undefined) return false;
|
|
22
|
+
return coversAnyRoute(projection, required.routes);
|
|
23
|
+
}
|
package/src/plugin/registry.ts
CHANGED
|
@@ -14,7 +14,9 @@ import type {
|
|
|
14
14
|
PluginEntry,
|
|
15
15
|
RegisteredNavEntry,
|
|
16
16
|
RegisteredPage,
|
|
17
|
+
RegisteredRoutePanel,
|
|
17
18
|
RegisteredSettingsTab,
|
|
19
|
+
RoutePanelContribution,
|
|
18
20
|
SettingsTabContribution,
|
|
19
21
|
ToolPanelContribution,
|
|
20
22
|
} from './types';
|
|
@@ -23,6 +25,18 @@ const toolPanels = new Map<string, ToolPanelContribution>();
|
|
|
23
25
|
const pages: RegisteredPage[] = [];
|
|
24
26
|
const settingsTabs: RegisteredSettingsTab[] = [];
|
|
25
27
|
const navEntries: RegisteredNavEntry[] = [];
|
|
28
|
+
const routePanels = new Map<string, RegisteredRoutePanel>();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What the host knows about a plugin from its registry listing entry: `mounts`, the
|
|
32
|
+
* plugin's own route items and their resolved absolute bases (`{item: "/api/<base>"}`),
|
|
33
|
+
* and `declaredRoutePanels`, the channels its manifest lists under
|
|
34
|
+
* `contributions.route_panels`.
|
|
35
|
+
*/
|
|
36
|
+
export interface PluginLoadInputs {
|
|
37
|
+
readonly mounts: Readonly<Record<string, string>>;
|
|
38
|
+
readonly declaredRoutePanels: readonly string[];
|
|
39
|
+
}
|
|
26
40
|
|
|
27
41
|
/** The contributions one plugin stages before they commit as a batch. */
|
|
28
42
|
interface StagedContributions {
|
|
@@ -31,6 +45,7 @@ interface StagedContributions {
|
|
|
31
45
|
readonly settingsTabs: RegisteredSettingsTab[];
|
|
32
46
|
readonly tabIds: Set<string>;
|
|
33
47
|
readonly navEntries: RegisteredNavEntry[];
|
|
48
|
+
readonly routePanels: Map<string, RegisteredRoutePanel>;
|
|
34
49
|
}
|
|
35
50
|
|
|
36
51
|
/**
|
|
@@ -48,6 +63,7 @@ function emptyStaged(): StagedContributions {
|
|
|
48
63
|
settingsTabs: [],
|
|
49
64
|
tabIds: new Set(),
|
|
50
65
|
navEntries: [],
|
|
66
|
+
routePanels: new Map(),
|
|
51
67
|
};
|
|
52
68
|
}
|
|
53
69
|
|
|
@@ -63,6 +79,7 @@ function makeStagingContext(
|
|
|
63
79
|
pluginId: string,
|
|
64
80
|
staged: StagedContributions,
|
|
65
81
|
seal: Seal,
|
|
82
|
+
host: PluginLoadInputs,
|
|
66
83
|
): PluginContext {
|
|
67
84
|
const assertOpen = (): void => {
|
|
68
85
|
if (seal.closed) {
|
|
@@ -120,6 +137,16 @@ function makeStagingContext(
|
|
|
120
137
|
}
|
|
121
138
|
staged.navEntries.push({ ...contribution, pluginId });
|
|
122
139
|
},
|
|
140
|
+
registerRoutePanel(contribution: RoutePanelContribution): void {
|
|
141
|
+
assertOpen();
|
|
142
|
+
if (staged.routePanels.has(contribution.channel) || routePanels.has(contribution.channel)) {
|
|
143
|
+
throw new Error(`a route panel is already registered for channel ${contribution.channel}`);
|
|
144
|
+
}
|
|
145
|
+
staged.routePanels.set(contribution.channel, { ...contribution, pluginId });
|
|
146
|
+
},
|
|
147
|
+
routeBase(itemName: string): string | null {
|
|
148
|
+
return Object.hasOwn(host.mounts, itemName) ? (host.mounts[itemName] ?? null) : null;
|
|
149
|
+
},
|
|
123
150
|
};
|
|
124
151
|
}
|
|
125
152
|
|
|
@@ -142,6 +169,27 @@ function assertNavEntriesHavePages(pluginId: string, staged: StagedContributions
|
|
|
142
169
|
}
|
|
143
170
|
}
|
|
144
171
|
|
|
172
|
+
/**
|
|
173
|
+
* The channels a plugin registered route panels for must be exactly the channels its
|
|
174
|
+
* manifest declares: a bundle and its manifest are built together, so a mismatch is a
|
|
175
|
+
* build fault. Throwing BEFORE any commit keeps atomicity.
|
|
176
|
+
*/
|
|
177
|
+
function assertRoutePanelsDeclared(
|
|
178
|
+
pluginId: string,
|
|
179
|
+
staged: StagedContributions,
|
|
180
|
+
declared: readonly string[],
|
|
181
|
+
): void {
|
|
182
|
+
const registered = [...staged.routePanels.keys()].sort();
|
|
183
|
+
const manifest = [...new Set(declared)].sort();
|
|
184
|
+
const same =
|
|
185
|
+
registered.length === manifest.length && registered.every((c, i) => c === manifest[i]);
|
|
186
|
+
if (!same) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`plugin “${pluginId}” registered route panels [${registered.join(', ')}] but its manifest declares [${manifest.join(', ')}]`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
145
193
|
/**
|
|
146
194
|
* Commit the staged contributions as one batch, so the registry only ever holds a
|
|
147
195
|
* plugin's complete set — never a partial set left behind by a mid-registration failure.
|
|
@@ -151,6 +199,7 @@ function commitStaged(staged: StagedContributions): void {
|
|
|
151
199
|
for (const [toolName, contribution] of staged.toolPanels) toolPanels.set(toolName, contribution);
|
|
152
200
|
for (const tab of staged.settingsTabs) settingsTabs.push(tab);
|
|
153
201
|
for (const navEntry of staged.navEntries) navEntries.push(navEntry);
|
|
202
|
+
for (const [channel, panel] of staged.routePanels) routePanels.set(channel, panel);
|
|
154
203
|
}
|
|
155
204
|
|
|
156
205
|
/**
|
|
@@ -164,10 +213,14 @@ function commitStaged(staged: StagedContributions): void {
|
|
|
164
213
|
* during `entry`'s synchronous body — including a microtask it enqueues then — are
|
|
165
214
|
* still captured and committed. A `register` that throws commits nothing.
|
|
166
215
|
*/
|
|
167
|
-
export async function loadPlugin(
|
|
216
|
+
export async function loadPlugin(
|
|
217
|
+
pluginId: string,
|
|
218
|
+
entry: PluginEntry,
|
|
219
|
+
host: PluginLoadInputs,
|
|
220
|
+
): Promise<void> {
|
|
168
221
|
const staged = emptyStaged();
|
|
169
222
|
const seal: Seal = { closed: false };
|
|
170
|
-
const context = makeStagingContext(pluginId, staged, seal);
|
|
223
|
+
const context = makeStagingContext(pluginId, staged, seal, host);
|
|
171
224
|
|
|
172
225
|
try {
|
|
173
226
|
await entry(context);
|
|
@@ -178,12 +231,13 @@ export async function loadPlugin(pluginId: string, entry: PluginEntry): Promise<
|
|
|
178
231
|
}
|
|
179
232
|
|
|
180
233
|
assertNavEntriesHavePages(pluginId, staged);
|
|
234
|
+
assertRoutePanelsDeclared(pluginId, staged, host.declaredRoutePanels);
|
|
181
235
|
commitStaged(staged);
|
|
182
236
|
}
|
|
183
237
|
|
|
184
238
|
/** The shell reads this after loading every installed plugin bundle. */
|
|
185
239
|
export function getContributions(): PluginContributions {
|
|
186
|
-
return { toolPanels, pages, settingsTabs, navEntries };
|
|
240
|
+
return { toolPanels, pages, settingsTabs, navEntries, routePanels };
|
|
187
241
|
}
|
|
188
242
|
|
|
189
243
|
/** Tests reset the module-level registry between cases. */
|
|
@@ -192,4 +246,5 @@ export function __resetContributions(): void {
|
|
|
192
246
|
pages.length = 0;
|
|
193
247
|
settingsTabs.length = 0;
|
|
194
248
|
navEntries.length = 0;
|
|
249
|
+
routePanels.clear();
|
|
195
250
|
}
|
package/src/plugin/types.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The Studio-plugin authoring contract. A platform plugin ships an ESM bundle
|
|
3
3
|
* that exports a `register(context)` entry ({@link PluginEntry}); the host calls
|
|
4
4
|
* it with a {@link PluginContext} through which the plugin contributes tool
|
|
5
|
-
* panels, pages, settings tabs,
|
|
5
|
+
* panels, pages, settings tabs, sidebar nav entries and conversation route panels.
|
|
6
6
|
*
|
|
7
7
|
* Two styling paths are sanctioned, and only these two:
|
|
8
8
|
* 1. SDK components plus inline styles that read the design-system tokens
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* its own. Bundled third-party base CSS is exempt from rule (c) but rules
|
|
21
21
|
* (a) and (b) still bind it.
|
|
22
22
|
*/
|
|
23
|
+
import type { ConversationRouteView } from '@tai42/api-client';
|
|
23
24
|
import type { ComponentType } from 'react';
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -219,12 +220,43 @@ export interface RegisteredSettingsTab extends SettingsTabContribution {
|
|
|
219
220
|
readonly pluginId: string;
|
|
220
221
|
}
|
|
221
222
|
|
|
223
|
+
/** A conversation route row as `GET /api/conversations` lists it, on the `channel` door. */
|
|
224
|
+
export type ChannelConversationRoute = Extract<ConversationRouteView, { readonly door: 'channel' }>;
|
|
225
|
+
|
|
226
|
+
/** Props a contributed route panel receives. */
|
|
227
|
+
export interface RoutePanelProps {
|
|
228
|
+
readonly pluginId: string;
|
|
229
|
+
/** The route row the panel decorates; its `channel` is the contribution's channel. */
|
|
230
|
+
readonly route: ChannelConversationRoute;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* A panel the conversations drill-in renders above the thread list of every route on
|
|
235
|
+
* one channel. One panel per channel deployment-wide.
|
|
236
|
+
*/
|
|
237
|
+
export interface RoutePanelContribution {
|
|
238
|
+
/** The channel name whose routes this panel decorates (`ConversationRouteView.channel`). */
|
|
239
|
+
readonly channel: string;
|
|
240
|
+
/** Names the panel region (its aria-label and the error card label). */
|
|
241
|
+
readonly title: string;
|
|
242
|
+
readonly component: ComponentType<RoutePanelProps>;
|
|
243
|
+
/** Capability gate (see {@link RequiredCapabilities}); absent ⇒ full-only. */
|
|
244
|
+
readonly requiredCapabilities?: RequiredCapabilities;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** A route panel as stored in the registry, stamped with the plugin that registered it. */
|
|
248
|
+
export interface RegisteredRoutePanel extends RoutePanelContribution {
|
|
249
|
+
readonly pluginId: string;
|
|
250
|
+
}
|
|
251
|
+
|
|
222
252
|
/** Everything a single plugin bundle has registered at import time. */
|
|
223
253
|
export interface PluginContributions {
|
|
224
254
|
readonly toolPanels: ReadonlyMap<string, ToolPanelContribution>;
|
|
225
255
|
readonly pages: readonly RegisteredPage[];
|
|
226
256
|
readonly settingsTabs: readonly RegisteredSettingsTab[];
|
|
227
257
|
readonly navEntries: readonly RegisteredNavEntry[];
|
|
258
|
+
/** Route panels keyed by the channel they decorate. */
|
|
259
|
+
readonly routePanels: ReadonlyMap<string, RegisteredRoutePanel>;
|
|
228
260
|
}
|
|
229
261
|
|
|
230
262
|
/**
|
|
@@ -250,6 +282,18 @@ export interface PluginContext {
|
|
|
250
282
|
* registers (a nav entry with no page is a dead link and is rejected loudly).
|
|
251
283
|
*/
|
|
252
284
|
registerNavEntry(contribution: NavEntryContribution): void;
|
|
285
|
+
/**
|
|
286
|
+
* Register a panel for the routes of one channel. The plugin's manifest must list the
|
|
287
|
+
* same channels under `contributions.route_panels`, or the plugin fails to load.
|
|
288
|
+
*/
|
|
289
|
+
registerRoutePanel(contribution: RoutePanelContribution): void;
|
|
290
|
+
/**
|
|
291
|
+
* The resolved absolute base of one of this plugin's route items, e.g. `/api/channels/web`,
|
|
292
|
+
* or `null` when the item has no mount on this deployment (an optional router not listed, a
|
|
293
|
+
* channel not loaded). Never throws; a feature that needs the base shows its own unavailable
|
|
294
|
+
* state for `null`. Call it during `register` and keep the value.
|
|
295
|
+
*/
|
|
296
|
+
routeBase(itemName: string): string | null;
|
|
253
297
|
}
|
|
254
298
|
|
|
255
299
|
/**
|
|
@@ -9,12 +9,14 @@ it; holds no edge to jq (the jq fields render through the host's ambient
|
|
|
9
9
|
|
|
10
10
|
- **template `input` jq** — input is the record (`.`), declared params bound as
|
|
11
11
|
`$params`; returns the injected value.
|
|
12
|
-
- **custom injection jq** — over
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
- **custom injection jq** — over the record (`.`), the run input bound as `$input`; returns
|
|
13
|
+
the value placed at `into`.
|
|
14
|
+
- **adapter** — over the tool output (`.`), the run input bound as `$input`; CONSTRUCTS the
|
|
15
|
+
update jq's declared input object.
|
|
16
|
+
- **template `update` jq** — over the record subtree (`.`), the adapter's output bound as
|
|
17
|
+
`$input`; returns a template-relative op batch.
|
|
18
|
+
- **custom `update` jq** — over the tool output (`.`), the run input bound as `$input` and the
|
|
19
|
+
record as `$record`; returns `[{op, path, value}]`.
|
|
18
20
|
- **`subject_expr`** — a jq yielding the record KEY: a bare key string, or a full subject object.
|
|
19
21
|
- **`scope_expr`** — optional; a boolean jq predicate. When it is false, the state is skipped for the run.
|
|
20
22
|
|
|
@@ -28,7 +30,8 @@ canonical, parseable object so a stored adapter reopens as the form it was autho
|
|
|
28
30
|
```
|
|
29
31
|
|
|
30
32
|
- `<key>` — a bare identifier, or a JSON string when it is not one.
|
|
31
|
-
- `<value>` — one of: a field path (
|
|
33
|
+
- `<value>` — one of: a field path (an output field `.…`, the whole output `.`, an input
|
|
34
|
+
field `$input…`), a JSON literal, or a raw
|
|
32
35
|
jq expression, each wrapped in `(…)`.
|
|
33
36
|
|
|
34
37
|
`parseAdapter` inverts it: `parseAdapter(compileAdapter(rows).jq) === rows` for every
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The update list of one attached state. Each update applies AFTER the run: either
|
|
3
3
|
* a template `update` jq (picked by name, whose declared input is authored through
|
|
4
|
-
* the adapter mapping) or a custom jq over
|
|
5
|
-
* the whole op batch itself. An optional op-id expression carries idempotency.
|
|
4
|
+
* the adapter mapping) or a custom jq over the tool output (its `.`, with the run input
|
|
5
|
+
* bound as `$input` and the record as `$record`) that authors the whole op batch itself. An optional op-id expression carries idempotency.
|
|
6
6
|
*/
|
|
7
7
|
import type { ReactNode } from 'react';
|
|
8
8
|
|
|
@@ -114,7 +114,7 @@ function UpdateRowEditor({
|
|
|
114
114
|
<BindingTemplatedJqField
|
|
115
115
|
label={`Custom update jq ${String(index + 1)}`}
|
|
116
116
|
required
|
|
117
|
-
description="A jq over
|
|
117
|
+
description="A jq over the tool output (`.`), with `$input` and `$record` bound, returning a template-relative op batch."
|
|
118
118
|
value={update.jq}
|
|
119
119
|
onChange={(next) => {
|
|
120
120
|
onChange({
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A named template `update` jq declares an input object (its `params` / declared
|
|
5
5
|
* input keys). A binding must map the run's OUTPUT (and the run's INPUT) into that
|
|
6
|
-
* object; that mapping is the `adapter` — a single jq program over
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* object; that mapping is the `adapter` — a single jq program over the tool output
|
|
7
|
+
* (its `.`) with the run input bound as `$input`, that CONSTRUCTS the update jq's
|
|
8
|
+
* declared input (which the update jq then reads as `$input`, over the record).
|
|
9
9
|
*
|
|
10
10
|
* The editor authors the adapter as a ROW form: one row per declared input key,
|
|
11
11
|
* each row picking one of three value sources — a picked field (from the run's
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
* object.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
/** Which
|
|
20
|
+
/** Which value a picked field reads: the tool output (the program's `.`) or the run input (`$input`). */
|
|
21
21
|
export type FieldRoot = 'output' | 'input';
|
|
22
22
|
|
|
23
|
-
/** A picked field: a path into
|
|
23
|
+
/** A picked field: a path into the tool output or the run input. */
|
|
24
24
|
export interface FieldSource {
|
|
25
25
|
readonly kind: 'field';
|
|
26
26
|
readonly root: FieldRoot;
|
|
@@ -33,7 +33,7 @@ export interface LiteralSource {
|
|
|
33
33
|
readonly json: string;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/** A raw jq expression over
|
|
36
|
+
/** A raw jq expression over the tool output (its `.`), with the run input bound as `$input`. */
|
|
37
37
|
export interface JqSource {
|
|
38
38
|
readonly kind: 'jq';
|
|
39
39
|
readonly expr: string;
|
|
@@ -59,13 +59,19 @@ export function jqKey(key: string): string {
|
|
|
59
59
|
return IDENTIFIER.test(key) ? key : JSON.stringify(key);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* A picked field rendered as a jq path: an output field reads off the program root
|
|
64
|
+
* (`.user.name`, `.["odd key"]`, the whole output `.`), an input field off the bound run
|
|
65
|
+
* input (`$input.user.name`, the whole input `$input`).
|
|
66
|
+
*/
|
|
63
67
|
export function fieldPathToJq(root: FieldRoot, path: readonly string[]): string {
|
|
64
|
-
let out =
|
|
68
|
+
let out = root === 'input' ? '$input' : '';
|
|
65
69
|
for (const segment of path) {
|
|
66
|
-
out += IDENTIFIER.test(segment)
|
|
70
|
+
out += IDENTIFIER.test(segment)
|
|
71
|
+
? `.${segment}`
|
|
72
|
+
: `${out === '' ? '.' : ''}[${JSON.stringify(segment)}]`;
|
|
67
73
|
}
|
|
68
|
-
return out;
|
|
74
|
+
return out === '' ? '.' : out;
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
/** The jq fragment ONE row's value compiles to — the row's "show jq". */
|
|
@@ -95,7 +101,7 @@ export function rowValueJq(source: MappingSource): AdapterCompileResult {
|
|
|
95
101
|
|
|
96
102
|
/**
|
|
97
103
|
* Compile a mapping form into ONE adapter jq that constructs the update jq's
|
|
98
|
-
* declared input over
|
|
104
|
+
* declared input over the tool output and `$input`. An empty form compiles to the
|
|
99
105
|
* empty object `{}`; a blank target, a duplicate target, or an invalid source is a
|
|
100
106
|
* loud error.
|
|
101
107
|
*/
|
|
@@ -128,7 +134,7 @@ export function defaultRowsForInput(params: readonly string[]): MappingRow[] {
|
|
|
128
134
|
// `compileAdapter` emits a canonical, PARSEABLE shape so a stored adapter reopens as
|
|
129
135
|
// the form it was authored in (never UI-only state): the object
|
|
130
136
|
// `{ <key>: (<value>), … }` where `<key>` is a bare identifier or a JSON string and
|
|
131
|
-
// `<value>` is one of — a field path
|
|
137
|
+
// `<value>` is one of — a field path `.…` (output) / `$input…` (input), a JSON literal, or a raw
|
|
132
138
|
// jq expression. `parseAdapter` inverts it; `parse(compileAdapter(rows).jq) === rows`
|
|
133
139
|
// for every row kind whose literal is already canonical JSON. A shape it does not
|
|
134
140
|
// recognise returns `null` — the editor falls back to the raw-jq escape hatch.
|
|
@@ -139,10 +145,19 @@ const ADAPTER_KEY = /^("(?:[^"\\]|\\.)*"|[A-Za-z_][A-Za-z0-9_]*)\s*:\s*\(/;
|
|
|
139
145
|
|
|
140
146
|
/** Recover a field source from a pure path expression, or `null` if it is not one. */
|
|
141
147
|
export function parseFieldPath(value: string): FieldSource | null {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
let root: FieldRoot;
|
|
149
|
+
let rest: string;
|
|
150
|
+
if (value === '.') return { kind: 'field', root: 'output', path: [] };
|
|
151
|
+
if (value.startsWith('$input')) {
|
|
152
|
+
root = 'input';
|
|
153
|
+
rest = value.slice('$input'.length);
|
|
154
|
+
} else if (value.startsWith('.')) {
|
|
155
|
+
root = 'output';
|
|
156
|
+
// A root bracket (`.["odd key"]`) drops its leading dot; a root key keeps it (`.a`).
|
|
157
|
+
rest = value.startsWith('.[') ? value.slice(1) : value;
|
|
158
|
+
} else {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
146
161
|
const path: string[] = [];
|
|
147
162
|
while (rest.length > 0) {
|
|
148
163
|
if (rest.startsWith('.')) {
|