@tai42/studio-sdk 25.0.1 → 26.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/README.md +143 -26
- package/dist/components/action-menu.d.ts +30 -0
- package/dist/components/action-menu.d.ts.map +1 -0
- package/dist/components/action-menu.js +101 -0
- package/dist/components/action-menu.js.map +1 -0
- package/dist/components/agent-picker.d.ts +22 -0
- package/dist/components/agent-picker.d.ts.map +1 -0
- package/dist/components/agent-picker.js +17 -0
- package/dist/components/agent-picker.js.map +1 -0
- package/dist/components/channel-picker.d.ts +27 -0
- package/dist/components/channel-picker.d.ts.map +1 -0
- package/dist/components/channel-picker.js +26 -0
- package/dist/components/channel-picker.js.map +1 -0
- package/dist/components/components.css +226 -8
- package/dist/components/confirm-dialog.d.ts +3 -1
- package/dist/components/confirm-dialog.d.ts.map +1 -1
- package/dist/components/confirm-dialog.js +2 -2
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/execution-key-picker.d.ts +26 -10
- package/dist/components/execution-key-picker.d.ts.map +1 -1
- package/dist/components/execution-key-picker.js +134 -15
- package/dist/components/execution-key-picker.js.map +1 -1
- package/dist/components/extension-combo-builder.d.ts +7 -7
- package/dist/components/extension-combo-builder.d.ts.map +1 -1
- package/dist/components/extension-combo-builder.js +42 -16
- package/dist/components/extension-combo-builder.js.map +1 -1
- package/dist/components/extension-combo-parts.d.ts +13 -11
- package/dist/components/extension-combo-parts.d.ts.map +1 -1
- package/dist/components/extension-combo-parts.js +10 -19
- package/dist/components/extension-combo-parts.js.map +1 -1
- package/dist/components/extension-config-editor.d.ts +33 -0
- package/dist/components/extension-config-editor.d.ts.map +1 -0
- package/dist/components/extension-config-editor.js +190 -0
- package/dist/components/extension-config-editor.js.map +1 -0
- package/dist/components/extension-grouping.d.ts +4 -41
- package/dist/components/extension-grouping.d.ts.map +1 -1
- package/dist/components/extension-grouping.js +1 -48
- package/dist/components/extension-grouping.js.map +1 -1
- package/dist/components/extension-picker.d.ts +4 -3
- package/dist/components/extension-picker.d.ts.map +1 -1
- package/dist/components/extension-picker.js +6 -6
- package/dist/components/extension-picker.js.map +1 -1
- package/dist/components/fleet-report.d.ts +8 -3
- package/dist/components/fleet-report.d.ts.map +1 -1
- package/dist/components/fleet-report.js +12 -2
- package/dist/components/fleet-report.js.map +1 -1
- package/dist/components/holder-kinds.d.ts +18 -0
- package/dist/components/holder-kinds.d.ts.map +1 -0
- package/dist/components/holder-kinds.js +41 -0
- package/dist/components/holder-kinds.js.map +1 -0
- package/dist/components/holder-list.d.ts +58 -0
- package/dist/components/holder-list.d.ts.map +1 -0
- package/dist/components/holder-list.js +77 -0
- package/dist/components/holder-list.js.map +1 -0
- 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 +1 -3
- package/dist/components/icons/nav-map.js.map +1 -1
- package/dist/components/icons/nav-marks.d.ts +0 -4
- package/dist/components/icons/nav-marks.d.ts.map +1 -1
- package/dist/components/icons/nav-marks.js +0 -4
- package/dist/components/icons/nav-marks.js.map +1 -1
- package/dist/components/named-list-picker.d.ts +39 -0
- package/dist/components/named-list-picker.d.ts.map +1 -0
- package/dist/components/named-list-picker.js +61 -0
- package/dist/components/named-list-picker.js.map +1 -0
- package/dist/components/named-list-test-utils.d.ts +23 -0
- package/dist/components/named-list-test-utils.d.ts.map +1 -0
- package/dist/components/named-list-test-utils.js +33 -0
- package/dist/components/named-list-test-utils.js.map +1 -0
- package/dist/components/recycle-roll.d.ts +23 -0
- package/dist/components/recycle-roll.d.ts.map +1 -0
- package/dist/components/recycle-roll.js +47 -0
- package/dist/components/recycle-roll.js.map +1 -0
- package/dist/components/stepper.d.ts +28 -0
- package/dist/components/stepper.d.ts.map +1 -0
- package/dist/components/stepper.js +33 -0
- package/dist/components/stepper.js.map +1 -0
- package/dist/components/subject-section.d.ts +2 -1
- package/dist/components/subject-section.d.ts.map +1 -1
- package/dist/components/subject-section.js +10 -5
- package/dist/components/subject-section.js.map +1 -1
- package/dist/components/tokens.css +31 -41
- package/dist/components/tokens.d.ts +3 -4
- package/dist/components/tokens.d.ts.map +1 -1
- package/dist/components/tokens.js +5 -13
- package/dist/components/tokens.js.map +1 -1
- package/dist/components/tool-picker-parts.d.ts +5 -1
- package/dist/components/tool-picker-parts.d.ts.map +1 -1
- package/dist/components/tool-picker-parts.js +8 -6
- package/dist/components/tool-picker-parts.js.map +1 -1
- package/dist/components/tool-picker.d.ts +77 -2
- package/dist/components/tool-picker.d.ts.map +1 -1
- package/dist/components/tool-picker.js +87 -29
- package/dist/components/tool-picker.js.map +1 -1
- package/dist/components/version-history-panel.d.ts +7 -1
- package/dist/components/version-history-panel.d.ts.map +1 -1
- package/dist/components/version-history-panel.js +2 -2
- package/dist/components/version-history-panel.js.map +1 -1
- package/dist/components/version-history-rows.d.ts +4 -2
- package/dist/components/version-history-rows.d.ts.map +1 -1
- package/dist/components/version-history-rows.js +14 -2
- package/dist/components/version-history-rows.js.map +1 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +7 -0
- package/dist/errors.js.map +1 -1
- package/dist/hooks/useApi.d.ts +524 -197
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/hooks/useCapabilities.d.ts +43 -10
- package/dist/hooks/useCapabilities.d.ts.map +1 -1
- package/dist/hooks/useCapabilities.js +39 -10
- package/dist/hooks/useCapabilities.js.map +1 -1
- package/dist/index.d.ts +25 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -6
- package/dist/index.js.map +1 -1
- package/dist/marketplace-install/env-var-fields.d.ts +17 -0
- package/dist/marketplace-install/env-var-fields.d.ts.map +1 -0
- package/dist/marketplace-install/env-var-fields.js +22 -0
- package/dist/marketplace-install/env-var-fields.js.map +1 -0
- package/dist/marketplace-install/index.d.ts +18 -0
- package/dist/marketplace-install/index.d.ts.map +1 -0
- package/dist/marketplace-install/index.js +8 -0
- package/dist/marketplace-install/index.js.map +1 -0
- package/dist/marketplace-install/install-env.d.ts +21 -0
- package/dist/marketplace-install/install-env.d.ts.map +1 -0
- package/dist/marketplace-install/install-env.js +22 -0
- package/dist/marketplace-install/install-env.js.map +1 -0
- package/dist/marketplace-install/install-mount.d.ts +42 -0
- package/dist/marketplace-install/install-mount.d.ts.map +1 -0
- package/dist/marketplace-install/install-mount.js +62 -0
- package/dist/marketplace-install/install-mount.js.map +1 -0
- package/dist/marketplace-install/install-report.d.ts +14 -0
- package/dist/marketplace-install/install-report.d.ts.map +1 -0
- package/dist/marketplace-install/install-report.js +19 -0
- package/dist/marketplace-install/install-report.js.map +1 -0
- package/dist/marketplace-install/install-shell.d.ts +34 -0
- package/dist/marketplace-install/install-shell.d.ts.map +1 -0
- package/dist/marketplace-install/install-shell.js +32 -0
- package/dist/marketplace-install/install-shell.js.map +1 -0
- package/dist/marketplace-install/mount-install.d.ts +41 -0
- package/dist/marketplace-install/mount-install.d.ts.map +1 -0
- package/dist/marketplace-install/mount-install.js +90 -0
- package/dist/marketplace-install/mount-install.js.map +1 -0
- package/dist/marketplace-install/plugin-detail.d.ts +17 -0
- package/dist/marketplace-install/plugin-detail.d.ts.map +1 -0
- package/dist/marketplace-install/plugin-detail.js +41 -0
- package/dist/marketplace-install/plugin-detail.js.map +1 -0
- package/dist/marketplace-install/plugin-install.d.ts +27 -0
- package/dist/marketplace-install/plugin-install.d.ts.map +1 -0
- package/dist/marketplace-install/plugin-install.js +89 -0
- package/dist/marketplace-install/plugin-install.js.map +1 -0
- package/dist/marketplace-install/route-items.d.ts +14 -0
- package/dist/marketplace-install/route-items.d.ts.map +1 -0
- package/dist/marketplace-install/route-items.js +11 -0
- package/dist/marketplace-install/route-items.js.map +1 -0
- package/dist/marketplace-install/use-debounced-value.d.ts +6 -0
- package/dist/marketplace-install/use-debounced-value.d.ts.map +1 -0
- package/dist/marketplace-install/use-debounced-value.js +18 -0
- package/dist/marketplace-install/use-debounced-value.js.map +1 -0
- package/dist/marketplace-install/use-install-action.d.ts +26 -0
- package/dist/marketplace-install/use-install-action.d.ts.map +1 -0
- package/dist/marketplace-install/use-install-action.js +35 -0
- package/dist/marketplace-install/use-install-action.js.map +1 -0
- package/dist/marketplace-install/use-install-preview.d.ts +33 -0
- package/dist/marketplace-install/use-install-preview.d.ts.map +1 -0
- package/dist/marketplace-install/use-install-preview.js +44 -0
- package/dist/marketplace-install/use-install-preview.js.map +1 -0
- package/dist/marketplace-install/use-mount-install.d.ts +80 -0
- package/dist/marketplace-install/use-mount-install.d.ts.map +1 -0
- package/dist/marketplace-install/use-mount-install.js +75 -0
- package/dist/marketplace-install/use-mount-install.js.map +1 -0
- package/dist/navigation/index.d.ts +1 -1
- package/dist/navigation/index.d.ts.map +1 -1
- package/dist/navigation/index.js.map +1 -1
- package/dist/navigation/types.d.ts +23 -7
- package/dist/navigation/types.d.ts.map +1 -1
- package/dist/plugin/registry.d.ts +0 -8
- package/dist/plugin/registry.d.ts.map +1 -1
- package/dist/plugin/registry.js +25 -1
- package/dist/plugin/registry.js.map +1 -1
- package/dist/plugin/types.d.ts +36 -12
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/version.d.ts +6 -4
- package/dist/plugin/version.d.ts.map +1 -1
- package/dist/plugin/version.js +6 -4
- package/dist/plugin/version.js.map +1 -1
- package/dist/query-keys.d.ts +45 -4
- package/dist/query-keys.d.ts.map +1 -1
- package/dist/query-keys.js +52 -4
- package/dist/query-keys.js.map +1 -1
- package/dist/schema-form/json-schema-meta.d.ts +3 -0
- package/dist/schema-form/json-schema-meta.d.ts.map +1 -0
- package/dist/schema-form/json-schema-meta.js +3 -0
- package/dist/schema-form/json-schema-meta.js.map +1 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +2 -0
- package/dist/testing.js.map +1 -1
- package/dist/token-rules.d.ts +26 -0
- package/dist/token-rules.d.ts.map +1 -0
- package/dist/token-rules.js +64 -0
- package/dist/token-rules.js.map +1 -0
- package/package.json +2 -2
- package/src/components/action-menu.tsx +160 -0
- package/src/components/agent-picker.tsx +53 -0
- package/src/components/channel-picker.tsx +68 -0
- package/src/components/components.css +226 -8
- package/src/components/confirm-dialog.tsx +4 -1
- package/src/components/execution-key-picker.tsx +252 -36
- package/src/components/extension-combo-builder.tsx +61 -29
- package/src/components/extension-combo-parts.tsx +28 -38
- package/src/components/extension-config-editor.tsx +307 -0
- package/src/components/extension-grouping.ts +4 -73
- package/src/components/extension-picker.tsx +13 -13
- package/src/components/fleet-report.tsx +38 -4
- package/src/components/holder-kinds.ts +60 -0
- package/src/components/holder-list.tsx +163 -0
- package/src/components/icons/index.ts +0 -2
- package/src/components/icons/nav-map.ts +0 -4
- package/src/components/icons/nav-marks.tsx +0 -15
- package/src/components/named-list-picker.tsx +182 -0
- package/src/components/named-list-test-utils.tsx +47 -0
- package/src/components/recycle-roll.tsx +120 -0
- package/src/components/stepper.tsx +111 -0
- package/src/components/subject-section.tsx +10 -5
- package/src/components/tokens.css +31 -41
- package/src/components/tokens.ts +5 -13
- package/src/components/tool-picker-parts.tsx +17 -6
- package/src/components/tool-picker.tsx +215 -41
- package/src/components/version-history-panel.tsx +8 -0
- package/src/components/version-history-rows.tsx +14 -2
- package/src/errors.ts +10 -0
- package/src/hooks/useCapabilities.ts +59 -10
- package/src/index.ts +54 -14
- package/src/marketplace-install/env-var-fields.tsx +59 -0
- package/src/marketplace-install/index.ts +17 -0
- package/src/marketplace-install/install-env.ts +39 -0
- package/src/marketplace-install/install-mount.ts +96 -0
- package/src/marketplace-install/install-report.tsx +54 -0
- package/src/marketplace-install/install-shell.tsx +99 -0
- package/src/marketplace-install/mount-install.tsx +290 -0
- package/src/marketplace-install/plugin-detail.ts +60 -0
- package/src/marketplace-install/plugin-install.tsx +221 -0
- package/src/marketplace-install/route-items.ts +20 -0
- package/src/marketplace-install/use-debounced-value.ts +18 -0
- package/src/marketplace-install/use-install-action.ts +60 -0
- package/src/marketplace-install/use-install-preview.ts +74 -0
- package/src/marketplace-install/use-mount-install.ts +116 -0
- package/src/navigation/index.ts +1 -0
- package/src/navigation/types.ts +27 -7
- package/src/plugin/registry.ts +19 -1
- package/src/plugin/types.ts +40 -12
- package/src/plugin/version.ts +6 -4
- package/src/query-keys.ts +71 -4
- package/src/schema-form/json-schema-meta.ts +2 -0
- package/src/testing.ts +2 -0
- package/src/token-rules.ts +66 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ExtensionConfigEditor` — the settings form of one drafted extension, driven by
|
|
3
|
+
* the extension's declared `config_schema`.
|
|
4
|
+
*
|
|
5
|
+
* The schema's top-level properties split in two: a property whose node `$ref`s the
|
|
6
|
+
* JSON Schema meta-schema holds a JSON Schema and is authored with a `SchemaEditor`;
|
|
7
|
+
* every other property is authored by one `SchemaForm`. A top-level setting whose
|
|
8
|
+
* seeded value holds an `!ENV` marker is sealed: it renders as a read-only row naming
|
|
9
|
+
* the variables, is emitted exactly as seeded, and counts as valid (the server checks
|
|
10
|
+
* the resolved value). A schema this editor cannot render — a meta-schema `$ref` below
|
|
11
|
+
* the top level, or a `$ref` outside the document — shows an error and reports invalid.
|
|
12
|
+
*
|
|
13
|
+
* It reports `{ config, valid }` through `onChange` on mount and on every edit.
|
|
14
|
+
*/
|
|
15
|
+
import { type ReactNode, useEffect, useRef, useState } from 'react';
|
|
16
|
+
|
|
17
|
+
import { collectEnvRefs } from '../env-markers';
|
|
18
|
+
import { SchemaEditor, type SchemaEditorChange } from '../schema-editor';
|
|
19
|
+
import { defaultValueForSchema } from '../schema-form/default-value';
|
|
20
|
+
import { JSON_SCHEMA_META_URI } from '../schema-form/json-schema-meta';
|
|
21
|
+
import { SchemaForm } from '../schema-form/SchemaForm';
|
|
22
|
+
import type { JsonSchema } from '../schema-form/types';
|
|
23
|
+
import { validateAgainstSchema } from '../schema-form/validate';
|
|
24
|
+
import { ErrorState } from './primitives';
|
|
25
|
+
|
|
26
|
+
export interface ExtensionConfigChange {
|
|
27
|
+
readonly config: Record<string, unknown>;
|
|
28
|
+
readonly valid: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ExtensionConfigEditorProps {
|
|
32
|
+
/** The extension's name; it names the settings group. */
|
|
33
|
+
readonly name: string;
|
|
34
|
+
/** The extension's declared `config_schema`. */
|
|
35
|
+
readonly schema: JsonSchema;
|
|
36
|
+
/** The element's stored settings when editing, else `undefined`. Read once, on mount. */
|
|
37
|
+
readonly value: Record<string, unknown> | undefined;
|
|
38
|
+
readonly onChange: (next: ExtensionConfigChange) => void;
|
|
39
|
+
readonly disabled?: boolean;
|
|
40
|
+
readonly idPrefix: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type SchemaSettings = Readonly<Record<string, SchemaEditorChange>>;
|
|
44
|
+
|
|
45
|
+
const isMetaRef = (node: JsonSchema): boolean => node.$ref === JSON_SCHEMA_META_URI;
|
|
46
|
+
|
|
47
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
48
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The sub-schemas of a node, each with its path. */
|
|
52
|
+
function childNodes(node: JsonSchema, path: string): [JsonSchema, string][] {
|
|
53
|
+
const children: [JsonSchema, string][] = [];
|
|
54
|
+
for (const [key, child] of Object.entries(node.properties ?? {})) {
|
|
55
|
+
children.push([child, `${path}.${key}`]);
|
|
56
|
+
}
|
|
57
|
+
if (node.items !== undefined) children.push([node.items, `${path}[]`]);
|
|
58
|
+
if (isRecord(node.additionalProperties)) {
|
|
59
|
+
children.push([node.additionalProperties, `${path}.*`]);
|
|
60
|
+
}
|
|
61
|
+
for (const keyword of ['anyOf', 'oneOf', 'allOf'] as const) {
|
|
62
|
+
(node[keyword] ?? []).forEach((child, index) => {
|
|
63
|
+
children.push([child, `${path}.${keyword}[${String(index)}]`]);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
for (const keyword of ['$defs', 'definitions'] as const) {
|
|
67
|
+
for (const [key, child] of Object.entries(node[keyword] ?? {})) {
|
|
68
|
+
children.push([child, `${path}.${keyword}.${key}`]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return children;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Why a (non top-level-setting) node cannot be rendered here, or `null`. */
|
|
75
|
+
function nestedReason(node: JsonSchema, path: string): string | null {
|
|
76
|
+
if (isMetaRef(node)) return `a JSON Schema setting is nested at ${path}`;
|
|
77
|
+
if (node.$ref !== undefined && !node.$ref.startsWith('#')) {
|
|
78
|
+
return `the reference ${node.$ref} at ${path} is outside this schema`;
|
|
79
|
+
}
|
|
80
|
+
for (const [child, childPath] of childNodes(node, path)) {
|
|
81
|
+
const reason = nestedReason(child, childPath);
|
|
82
|
+
if (reason !== null) return reason;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Why the declared schema cannot be rendered here, or `null` when it can. A
|
|
88
|
+
* meta-schema `$ref` is accepted only as a top-level property's own node. */
|
|
89
|
+
function unrenderableReason(schema: JsonSchema): string | null {
|
|
90
|
+
if (isMetaRef(schema)) return 'the whole settings value is a JSON Schema';
|
|
91
|
+
if (schema.$ref !== undefined && !schema.$ref.startsWith('#')) {
|
|
92
|
+
return `the reference ${schema.$ref} at $ is outside this schema`;
|
|
93
|
+
}
|
|
94
|
+
const topProperties = schema.properties ?? {};
|
|
95
|
+
for (const [child, childPath] of childNodes(schema, '$')) {
|
|
96
|
+
const key = childPath.slice(2);
|
|
97
|
+
if (topProperties[key] === child && isMetaRef(child)) continue;
|
|
98
|
+
const reason = nestedReason(child, childPath);
|
|
99
|
+
if (reason !== null) return reason;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface Split {
|
|
105
|
+
readonly formSchema: JsonSchema;
|
|
106
|
+
readonly schemaKeys: readonly string[];
|
|
107
|
+
readonly sealed: Readonly<Record<string, unknown>>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function splitSchema(schema: JsonSchema, seed: Record<string, unknown> | undefined): Split {
|
|
111
|
+
const properties = schema.properties ?? {};
|
|
112
|
+
const sealed: Record<string, unknown> = {};
|
|
113
|
+
if (seed !== undefined) {
|
|
114
|
+
for (const key of Object.keys(properties)) {
|
|
115
|
+
if (key in seed && collectEnvRefs(seed[key]).size > 0) sealed[key] = seed[key];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const schemaKeys: string[] = [];
|
|
119
|
+
const rest: Record<string, JsonSchema> = {};
|
|
120
|
+
for (const [key, node] of Object.entries(properties)) {
|
|
121
|
+
if (key in sealed) continue;
|
|
122
|
+
if (isMetaRef(node)) schemaKeys.push(key);
|
|
123
|
+
else rest[key] = node;
|
|
124
|
+
}
|
|
125
|
+
const formSchema: JsonSchema = {
|
|
126
|
+
...schema,
|
|
127
|
+
properties: rest,
|
|
128
|
+
required: (schema.required ?? []).filter((key) => key in rest),
|
|
129
|
+
};
|
|
130
|
+
return { formSchema, schemaKeys, sealed };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function settingLabel(schema: JsonSchema, key: string): string {
|
|
134
|
+
return schema.properties?.[key]?.title ?? key;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The read-only row of a sealed setting: its label and the env variables it names. */
|
|
138
|
+
function SealedRow({ label, value }: { readonly label: string; readonly value: unknown }) {
|
|
139
|
+
const names = [...collectEnvRefs(value)].join(', ');
|
|
140
|
+
return (
|
|
141
|
+
<div className="tai-stack tai-stack-1">
|
|
142
|
+
<span className="tai-field-label">{label}</span>
|
|
143
|
+
<span className="tai-muted">{`Env reference ${names}, kept as written.`}</span>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function SettingsGroup({
|
|
149
|
+
name,
|
|
150
|
+
disabled,
|
|
151
|
+
children,
|
|
152
|
+
}: {
|
|
153
|
+
readonly name: string;
|
|
154
|
+
readonly disabled?: boolean | undefined;
|
|
155
|
+
readonly children: ReactNode;
|
|
156
|
+
}) {
|
|
157
|
+
return (
|
|
158
|
+
<fieldset
|
|
159
|
+
className="tai-stack tai-stack-2"
|
|
160
|
+
style={{ border: 'none', margin: 0, padding: 0 }}
|
|
161
|
+
disabled={disabled}
|
|
162
|
+
>
|
|
163
|
+
<legend className="tai-field-label">{`${name} settings`}</legend>
|
|
164
|
+
{children}
|
|
165
|
+
</fieldset>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Keeps the latest `onChange` so the report effect runs on value changes only. */
|
|
170
|
+
function useLatest<T>(current: T): { readonly current: T } {
|
|
171
|
+
const ref = useRef(current);
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
ref.current = current;
|
|
174
|
+
});
|
|
175
|
+
return ref;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function ExtensionConfigEditor({
|
|
179
|
+
name,
|
|
180
|
+
schema,
|
|
181
|
+
value,
|
|
182
|
+
onChange,
|
|
183
|
+
disabled,
|
|
184
|
+
idPrefix,
|
|
185
|
+
}: ExtensionConfigEditorProps): ReactNode {
|
|
186
|
+
const [reason] = useState(() => unrenderableReason(schema));
|
|
187
|
+
if (reason !== null) {
|
|
188
|
+
return <UnrenderableSettings name={name} reason={reason} value={value} onChange={onChange} />;
|
|
189
|
+
}
|
|
190
|
+
return (
|
|
191
|
+
<RenderableSettings
|
|
192
|
+
name={name}
|
|
193
|
+
schema={schema}
|
|
194
|
+
value={value}
|
|
195
|
+
onChange={onChange}
|
|
196
|
+
disabled={disabled}
|
|
197
|
+
idPrefix={idPrefix}
|
|
198
|
+
/>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function UnrenderableSettings({
|
|
203
|
+
name,
|
|
204
|
+
reason,
|
|
205
|
+
value,
|
|
206
|
+
onChange,
|
|
207
|
+
}: {
|
|
208
|
+
readonly name: string;
|
|
209
|
+
readonly reason: string;
|
|
210
|
+
readonly value: Record<string, unknown> | undefined;
|
|
211
|
+
readonly onChange: (next: ExtensionConfigChange) => void;
|
|
212
|
+
}): ReactNode {
|
|
213
|
+
const latest = useLatest(onChange);
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
latest.current({ config: value ?? {}, valid: false });
|
|
216
|
+
}, [latest, value]);
|
|
217
|
+
return (
|
|
218
|
+
<SettingsGroup name={name}>
|
|
219
|
+
<ErrorState message={`The settings of ${name} cannot be edited here: ${reason}.`} />
|
|
220
|
+
</SettingsGroup>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function RenderableSettings({
|
|
225
|
+
name,
|
|
226
|
+
schema,
|
|
227
|
+
value,
|
|
228
|
+
onChange,
|
|
229
|
+
disabled,
|
|
230
|
+
idPrefix,
|
|
231
|
+
}: ExtensionConfigEditorProps): ReactNode {
|
|
232
|
+
const [split] = useState(() => splitSchema(schema, value));
|
|
233
|
+
const { formSchema, schemaKeys, sealed } = split;
|
|
234
|
+
const [formValue, setFormValue] = useState<unknown>(() => {
|
|
235
|
+
if (value === undefined) return defaultValueForSchema(formSchema);
|
|
236
|
+
const seeded: Record<string, unknown> = {};
|
|
237
|
+
for (const [key, stored] of Object.entries(value)) {
|
|
238
|
+
if (!schemaKeys.includes(key) && !(key in sealed)) seeded[key] = stored;
|
|
239
|
+
}
|
|
240
|
+
return seeded;
|
|
241
|
+
});
|
|
242
|
+
const [schemaSettings, setSchemaSettings] = useState<SchemaSettings>(() => {
|
|
243
|
+
const seeded: Record<string, SchemaEditorChange> = {};
|
|
244
|
+
for (const key of schemaKeys) {
|
|
245
|
+
const stored = value?.[key];
|
|
246
|
+
seeded[key] = { schema: isRecord(stored) ? stored : null, valid: true };
|
|
247
|
+
}
|
|
248
|
+
return seeded;
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const formErrors = validateAgainstSchema(formSchema, formValue);
|
|
252
|
+
const required = new Set(schema.required ?? []);
|
|
253
|
+
const valid =
|
|
254
|
+
Object.keys(formErrors).length === 0 &&
|
|
255
|
+
schemaKeys.every((key) => {
|
|
256
|
+
const setting = schemaSettings[key];
|
|
257
|
+
return (
|
|
258
|
+
setting !== undefined && setting.valid && (!required.has(key) || setting.schema !== null)
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const latest = useLatest(onChange);
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
const config: Record<string, unknown> = { ...(isRecord(formValue) ? formValue : {}) };
|
|
265
|
+
for (const key of schemaKeys) {
|
|
266
|
+
const setting = schemaSettings[key]?.schema;
|
|
267
|
+
if (setting !== null && setting !== undefined) config[key] = setting;
|
|
268
|
+
}
|
|
269
|
+
Object.assign(config, sealed);
|
|
270
|
+
latest.current({ config, valid });
|
|
271
|
+
}, [latest, formValue, schemaSettings, schemaKeys, sealed, valid]);
|
|
272
|
+
|
|
273
|
+
const hasFormFields = Object.keys(formSchema.properties ?? {}).length > 0;
|
|
274
|
+
return (
|
|
275
|
+
<SettingsGroup name={name} disabled={disabled}>
|
|
276
|
+
{Object.entries(sealed).map(([key, sealedValue]) => (
|
|
277
|
+
<SealedRow key={key} label={settingLabel(schema, key)} value={sealedValue} />
|
|
278
|
+
))}
|
|
279
|
+
{hasFormFields ? (
|
|
280
|
+
<SchemaForm
|
|
281
|
+
schema={formSchema}
|
|
282
|
+
value={formValue}
|
|
283
|
+
onChange={setFormValue}
|
|
284
|
+
errors={formErrors}
|
|
285
|
+
idPrefix={`${idPrefix}-form`}
|
|
286
|
+
/>
|
|
287
|
+
) : null}
|
|
288
|
+
{schemaKeys.map((key) => {
|
|
289
|
+
const node = schema.properties?.[key];
|
|
290
|
+
return (
|
|
291
|
+
<SchemaEditor
|
|
292
|
+
key={key}
|
|
293
|
+
value={schemaSettings[key]?.schema ?? null}
|
|
294
|
+
onChange={(change) => {
|
|
295
|
+
setSchemaSettings((current) => ({ ...current, [key]: change }));
|
|
296
|
+
}}
|
|
297
|
+
requireTitle={false}
|
|
298
|
+
label={settingLabel(schema, key)}
|
|
299
|
+
description={node?.description}
|
|
300
|
+
disabled={disabled}
|
|
301
|
+
idPrefix={`${idPrefix}-${key}`}
|
|
302
|
+
/>
|
|
303
|
+
);
|
|
304
|
+
})}
|
|
305
|
+
</SettingsGroup>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
@@ -1,30 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure catalog-shaping helpers for the
|
|
3
|
-
*
|
|
4
|
-
* `
|
|
5
|
-
* directly.
|
|
6
|
-
*
|
|
7
|
-
* The `/api/extensions` catalog is a flat list of `{ name, kind, multiple }` entries. Two
|
|
8
|
-
* independent groupings live here:
|
|
9
|
-
* - `groupIntoFamilies` groups a tool's composed branch VARIANTS (which share a
|
|
10
|
-
* base tool name and append underscore suffixes — `tool`, `tool_a`, `tool_a_b`)
|
|
11
|
-
* back into one FAMILY keyed by that base name (the catalog view);
|
|
12
|
-
* - `groupByKind` groups the catalog by extension KIND (the picker view), each
|
|
13
|
-
* group carrying the served `multiple` flag of its kind.
|
|
14
|
-
* `kindVariant` maps each serialized `kind` value to a design-system Badge variant.
|
|
2
|
+
* Pure catalog-shaping helpers for the extension catalog: `groupByKind` groups the flat
|
|
3
|
+
* `{ name, kind, multiple }` list by kind, each group carrying its kind's served
|
|
4
|
+
* `multiple` flag. Kept free of React so it is unit-tested directly.
|
|
15
5
|
*/
|
|
16
6
|
import type { Extension } from '@tai42/api-client';
|
|
17
7
|
|
|
18
|
-
import type { BadgeProps } from './badge';
|
|
19
|
-
|
|
20
|
-
/** One base tool and every extension entry that branches from it. */
|
|
21
|
-
export interface ExtensionFamily {
|
|
22
|
-
/** The base tool name (the segment before the first underscore). */
|
|
23
|
-
readonly base: string;
|
|
24
|
-
/** The family's entries, ordered by full name. */
|
|
25
|
-
readonly members: readonly Extension[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
8
|
/** One extension `kind` and every catalog entry of that kind. */
|
|
29
9
|
export interface ExtensionKindGroup {
|
|
30
10
|
/** The serialized kind value (`wrapper` / `transformer` / `backend`). */
|
|
@@ -37,41 +17,7 @@ export interface ExtensionKindGroup {
|
|
|
37
17
|
}
|
|
38
18
|
|
|
39
19
|
/**
|
|
40
|
-
*
|
|
41
|
-
* underscore, or the whole name when it has none. `tool`, `tool_a`, `tool_a_b`
|
|
42
|
-
* all resolve to `tool`; `x_chain` and `x_batch` both resolve to `x`.
|
|
43
|
-
*/
|
|
44
|
-
export function baseNameOf(name: string): string {
|
|
45
|
-
const separator = name.indexOf('_');
|
|
46
|
-
return separator === -1 ? name : name.slice(0, separator);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Group a flat extension list into families keyed by base tool name. Families
|
|
51
|
-
* are ordered by base name and each family's members by full name, so the render
|
|
52
|
-
* order is stable regardless of the server's list order.
|
|
53
|
-
*/
|
|
54
|
-
export function groupIntoFamilies(extensions: readonly Extension[]): ExtensionFamily[] {
|
|
55
|
-
const byBase = new Map<string, Extension[]>();
|
|
56
|
-
for (const extension of extensions) {
|
|
57
|
-
const base = baseNameOf(extension.name);
|
|
58
|
-
const members = byBase.get(base);
|
|
59
|
-
if (members) {
|
|
60
|
-
members.push(extension);
|
|
61
|
-
} else {
|
|
62
|
-
byBase.set(base, [extension]);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return [...byBase.entries()]
|
|
66
|
-
.map(([base, members]) => ({
|
|
67
|
-
base,
|
|
68
|
-
members: [...members].sort((a, b) => a.name.localeCompare(b.name)),
|
|
69
|
-
}))
|
|
70
|
-
.sort((a, b) => a.base.localeCompare(b.base));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Group a flat extension list by `kind`, for the picker. Groups are ordered by
|
|
20
|
+
* Group a flat extension list by `kind`. Groups are ordered by
|
|
75
21
|
* kind and each group's members by name, so the render order is stable regardless
|
|
76
22
|
* of the server's list order. Each group carries its kind's served `multiple`; a kind
|
|
77
23
|
* whose entries disagree is a catalog fault and throws.
|
|
@@ -99,18 +45,3 @@ export function groupByKind(extensions: readonly Extension[]): ExtensionKindGrou
|
|
|
99
45
|
}))
|
|
100
46
|
.sort((a, b) => a.kind.localeCompare(b.kind));
|
|
101
47
|
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Map a serialized extension `kind` to a Badge variant. Known skeleton kinds get
|
|
105
|
-
* a themed color; any unrecognized kind falls back to the neutral variant (the
|
|
106
|
-
* Badge itself also neutral-fallbacks, but choosing here keeps the mapping
|
|
107
|
-
* explicit and testable).
|
|
108
|
-
*/
|
|
109
|
-
export function kindVariant(kind: string): NonNullable<BadgeProps['variant']> {
|
|
110
|
-
const known: Record<string, NonNullable<BadgeProps['variant']>> = {
|
|
111
|
-
wrapper: 'primary',
|
|
112
|
-
transformer: 'success',
|
|
113
|
-
backend: 'warning',
|
|
114
|
-
};
|
|
115
|
-
return known[kind] ?? 'neutral';
|
|
116
|
-
}
|
|
@@ -6,24 +6,25 @@
|
|
|
6
6
|
* live in the SDK). It is PRESENTATIONAL: the caller fetches `available` and owns
|
|
7
7
|
* the selected `value`; this component fetches nothing.
|
|
8
8
|
*
|
|
9
|
-
* The catalog is GROUPED by `kind
|
|
9
|
+
* The catalog is GROUPED by `kind`: each kind is a group `Field` named
|
|
10
|
+
* "Kind: <kind>", described "(single-select)" when it is single-select. A kind the catalog serves as not `multiple` is
|
|
10
11
|
* SINGLE-SELECT — checking one of its extensions replaces any other of that kind
|
|
11
12
|
* already in the combo; a `multiple` kind is freely multi-select. Selection order is preserved (the combo is ordered — the
|
|
12
13
|
* stacking order that produces the branch tool name), so a newly-checked extension
|
|
13
14
|
* appends to the end and unchecking removes in place.
|
|
14
15
|
*
|
|
15
16
|
* SAFETY: an extension name AND its kind are server-supplied, so every name/kind
|
|
16
|
-
* renders as TEXT through the DS `Checkbox`/`
|
|
17
|
+
* renders as TEXT through the DS `Checkbox`/`Field` (React escapes it) — never an
|
|
17
18
|
* HTML sink. Pinned by a test.
|
|
18
19
|
*
|
|
19
20
|
* Geometry and ink come from the design-system layout classes (`tai-stack`,
|
|
20
|
-
* `tai-row
|
|
21
|
+
* `tai-row`) and `Field`; the component carries no palette of its own.
|
|
21
22
|
*/
|
|
22
23
|
import type { Extension } from '@tai42/api-client';
|
|
23
24
|
|
|
24
|
-
import { Badge } from './badge';
|
|
25
25
|
import { Checkbox } from './checkbox';
|
|
26
|
-
import { groupByKind
|
|
26
|
+
import { groupByKind } from './extension-grouping';
|
|
27
|
+
import { Field } from './field';
|
|
27
28
|
import { EmptyState } from './primitives';
|
|
28
29
|
|
|
29
30
|
export interface ExtensionPickerProps {
|
|
@@ -81,13 +82,12 @@ export function ExtensionPicker({
|
|
|
81
82
|
return (
|
|
82
83
|
<div data-testid={idPrefix} className="tai-stack">
|
|
83
84
|
{groups.map((group) => (
|
|
84
|
-
<
|
|
85
|
-
{
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
</div>
|
|
85
|
+
<Field
|
|
86
|
+
key={group.kind}
|
|
87
|
+
group
|
|
88
|
+
label={`Kind: ${group.kind}`}
|
|
89
|
+
description={group.multiple ? undefined : '(single-select)'}
|
|
90
|
+
>
|
|
91
91
|
<div className="tai-row">
|
|
92
92
|
{group.members.map((extension) => (
|
|
93
93
|
<Checkbox
|
|
@@ -101,7 +101,7 @@ export function ExtensionPicker({
|
|
|
101
101
|
/>
|
|
102
102
|
))}
|
|
103
103
|
</div>
|
|
104
|
-
</
|
|
104
|
+
</Field>
|
|
105
105
|
))}
|
|
106
106
|
</div>
|
|
107
107
|
);
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
* server-supplied string (a worker name, an error/detail message) renders as escaped
|
|
24
24
|
* React text, never an HTML sink.
|
|
25
25
|
*
|
|
26
|
+
* Whatever the status, a `spanReleaseError` (the config store did not release its exclusive
|
|
27
|
+
* write span after the persist) renders its own `role="alert"` panel with the store's
|
|
28
|
+
* error: the change landed, but a later write can block or be refused until the writer
|
|
29
|
+
* process exits.
|
|
30
|
+
*
|
|
26
31
|
* Whatever the status, a worker whose reload named recycle-class settings awaiting a
|
|
27
32
|
* respawn is listed in a warning status panel beside it: the reload applied every other
|
|
28
33
|
* change, and those settings take effect only when that worker restarts.
|
|
@@ -79,9 +84,9 @@ const OUTCOME_STATUS: Record<FleetFailureOutcome, StatusTone> = {
|
|
|
79
84
|
};
|
|
80
85
|
|
|
81
86
|
/**
|
|
82
|
-
* Render a fleet broadcast's failure state
|
|
83
|
-
* `null` for an absent report or a converged one with no
|
|
84
|
-
* (nothing to warn about) so callers can always mount it unconditionally beside their
|
|
87
|
+
* Render a fleet broadcast's failure state, an unreleased write span, and its workers
|
|
88
|
+
* awaiting a respawn. Returns `null` for an absent report or a converged one with no
|
|
89
|
+
* unreleased span and no worker awaiting a respawn (nothing to warn about) so callers can always mount it unconditionally beside their
|
|
85
90
|
* own success line.
|
|
86
91
|
*/
|
|
87
92
|
export interface FleetReportProps {
|
|
@@ -235,6 +240,26 @@ function AwaitingRespawnReport({
|
|
|
235
240
|
);
|
|
236
241
|
}
|
|
237
242
|
|
|
243
|
+
/**
|
|
244
|
+
* The loud panel for a write span the config store did not release: the change landed,
|
|
245
|
+
* but the store may hold the span until the writer process exits.
|
|
246
|
+
*/
|
|
247
|
+
function SpanReleaseReport({ error }: { readonly error: string }): ReactNode {
|
|
248
|
+
return (
|
|
249
|
+
<div role="alert" className="tai-error-state tai-stack tai-stack-2">
|
|
250
|
+
<strong className="tai-status tai-status-err">
|
|
251
|
+
<XCircleIcon />
|
|
252
|
+
The config store did not release its write lock
|
|
253
|
+
</strong>
|
|
254
|
+
<p>
|
|
255
|
+
The change was saved, but the store may hold its write lock until the server process that
|
|
256
|
+
wrote it exits. Until then another config change can wait or be refused.
|
|
257
|
+
</p>
|
|
258
|
+
<p className="tai-code-block tai-code-block-wrap">{error}</p>
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
238
263
|
function FailureReport({
|
|
239
264
|
summary,
|
|
240
265
|
action,
|
|
@@ -269,10 +294,19 @@ function FailureReport({
|
|
|
269
294
|
|
|
270
295
|
export function FleetReport({ summary, action = 'save' }: FleetReportProps): ReactNode {
|
|
271
296
|
if (summary === null) return null;
|
|
272
|
-
if (
|
|
297
|
+
if (
|
|
298
|
+
summary.status === 'converged' &&
|
|
299
|
+
summary.awaiting.length === 0 &&
|
|
300
|
+
summary.spanReleaseError === null
|
|
301
|
+
) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
273
304
|
return (
|
|
274
305
|
<>
|
|
275
306
|
<FailureReport summary={summary} action={action} />
|
|
307
|
+
{summary.spanReleaseError !== null ? (
|
|
308
|
+
<SpanReleaseReport error={summary.spanReleaseError} />
|
|
309
|
+
) : null}
|
|
276
310
|
{summary.awaiting.length > 0 ? <AwaitingRespawnReport awaiting={summary.awaiting} /> : null}
|
|
277
311
|
</>
|
|
278
312
|
);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The holder kinds the platform itself reports, and the route each one lands on. A
|
|
3
|
+
* plugin may register a link only for a kind outside this table.
|
|
4
|
+
*/
|
|
5
|
+
import type { ReferenceHolder } from '@tai42/api-client';
|
|
6
|
+
|
|
7
|
+
import type { RouteSearchByToken, RouteToken } from '../navigation/types';
|
|
8
|
+
|
|
9
|
+
/** A route token with its search, typed together. */
|
|
10
|
+
export type CoreHolderRoute = {
|
|
11
|
+
[T in RouteToken]: { readonly token: T; readonly search: RouteSearchByToken[T] };
|
|
12
|
+
}[RouteToken];
|
|
13
|
+
|
|
14
|
+
/** A locator field the holder's kind always carries; its absence is a malformed holder. */
|
|
15
|
+
function locatorField<T extends 'string' | 'number'>(
|
|
16
|
+
holder: ReferenceHolder,
|
|
17
|
+
key: string,
|
|
18
|
+
type: T,
|
|
19
|
+
): T extends 'string' ? string : number {
|
|
20
|
+
const value = holder.locator[key];
|
|
21
|
+
if (typeof value !== type) {
|
|
22
|
+
throw new Error(`a “${holder.kind}” holder carries no ${type} locator “${key}”`);
|
|
23
|
+
}
|
|
24
|
+
return value as T extends 'string' ? string : number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const CORE_ROUTES: Readonly<Record<string, (holder: ReferenceHolder) => CoreHolderRoute>> = {
|
|
28
|
+
preset: (h) => ({ token: 'presets', search: { preset: h.name } }),
|
|
29
|
+
'preset version': (h) => ({
|
|
30
|
+
token: 'presets',
|
|
31
|
+
search: { preset: h.name, version: locatorField(h, 'version', 'number') },
|
|
32
|
+
}),
|
|
33
|
+
hook: (h) => ({ token: 'hooks', search: { hook: h.name } }),
|
|
34
|
+
schedule: (h) => ({ token: 'scheduling', search: { schedule: h.name } }),
|
|
35
|
+
'conversation route': (h) => ({ token: 'conversations', search: { route: h.name } }),
|
|
36
|
+
'conversation config': (h) => ({
|
|
37
|
+
token: 'conversations',
|
|
38
|
+
search: { config: `${locatorField(h, 'target_kind', 'string')}:${h.name}` },
|
|
39
|
+
}),
|
|
40
|
+
'tool extensions': () => ({ token: 'manifest', search: {} }),
|
|
41
|
+
'parked interactions': () => ({ token: 'interactions', search: {} }),
|
|
42
|
+
'answered continuations': () => ({ token: 'interactions', search: {} }),
|
|
43
|
+
'state records': () => ({ token: 'states', search: {} }),
|
|
44
|
+
'pending save': () => ({ token: 'states', search: {} }),
|
|
45
|
+
'state declaration': (h) => ({ token: 'states', search: { state: h.name, tab: 'declaration' } }),
|
|
46
|
+
'state template': (h) => ({ token: 'states', search: { template: h.name } }),
|
|
47
|
+
'api key': () => ({ token: 'settings', search: {} }),
|
|
48
|
+
principal: () => ({ token: 'settings', search: {} }),
|
|
49
|
+
role: () => ({ token: 'settings', search: {} }),
|
|
50
|
+
setting: () => ({ token: 'settings', search: {} }),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Every kind the platform reports; a plugin cannot register a link for one. */
|
|
54
|
+
export const CORE_HOLDER_KINDS: ReadonlySet<string> = new Set(Object.keys(CORE_ROUTES));
|
|
55
|
+
|
|
56
|
+
/** The route a core holder lands on, or `null` for a kind outside the core table. */
|
|
57
|
+
export function coreHolderRoute(holder: ReferenceHolder): CoreHolderRoute | null {
|
|
58
|
+
const route = Object.hasOwn(CORE_ROUTES, holder.kind) ? CORE_ROUTES[holder.kind] : undefined;
|
|
59
|
+
return route === undefined ? null : route(holder);
|
|
60
|
+
}
|