@khorsheed/dsh-capability-catalog 0.1.95
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +286 -0
- package/README.zh.md +138 -0
- package/cordis.patch.yml +8 -0
- package/lib/client.js +10100 -0
- package/lib/index.js +4022 -0
- package/lib/invariant.js +25 -0
- package/lib/mcpConnector-C2TbkPBY.js +69 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +1417 -0
- package/lib/typert.remote-client.d.ts +68 -0
- package/lib/typert.remote-client.js +1136 -0
- package/lib/types/capabilities.d.ts +103 -0
- package/lib/types/capabilities.js +116 -0
- package/lib/types/channels.d.ts +41 -0
- package/lib/types/channels.js +75 -0
- package/lib/types/client/AddMcpDialog.d.ts +13 -0
- package/lib/types/client/AddMcpDialog.js +79 -0
- package/lib/types/client/AddSkillModal.d.ts +17 -0
- package/lib/types/client/AddSkillModal.js +138 -0
- package/lib/types/client/BundleFileTree.d.ts +7 -0
- package/lib/types/client/BundleFileTree.js +74 -0
- package/lib/types/client/CapabilityCatalogCard.d.ts +57 -0
- package/lib/types/client/CapabilityCatalogCard.js +479 -0
- package/lib/types/client/CredentialField.d.ts +14 -0
- package/lib/types/client/CredentialField.js +9 -0
- package/lib/types/client/McpCards.d.ts +13 -0
- package/lib/types/client/McpCards.js +12 -0
- package/lib/types/client/McpServerManageModal.d.ts +18 -0
- package/lib/types/client/McpServerManageModal.js +63 -0
- package/lib/types/client/MetadataRow.d.ts +10 -0
- package/lib/types/client/MetadataRow.js +6 -0
- package/lib/types/client/ModalShell.d.ts +16 -0
- package/lib/types/client/ModalShell.js +27 -0
- package/lib/types/client/ModeChips.d.ts +38 -0
- package/lib/types/client/ModeChips.js +42 -0
- package/lib/types/client/SchemaView.d.ts +11 -0
- package/lib/types/client/SchemaView.js +143 -0
- package/lib/types/client/SkillCards.d.ts +28 -0
- package/lib/types/client/SkillCards.js +43 -0
- package/lib/types/client/SkillDetailModal.d.ts +69 -0
- package/lib/types/client/SkillDetailModal.js +167 -0
- package/lib/types/client/ToolCards.d.ts +28 -0
- package/lib/types/client/ToolCards.js +52 -0
- package/lib/types/client/ToolDetailModal.d.ts +12 -0
- package/lib/types/client/ToolDetailModal.js +29 -0
- package/lib/types/client/icons.d.ts +26 -0
- package/lib/types/client/icons.js +10 -0
- package/lib/types/client/index.d.ts +21 -0
- package/lib/types/client/index.js +196 -0
- package/lib/types/client/locales.d.ts +392 -0
- package/lib/types/client/locales.js +395 -0
- package/lib/types/client/mcp-model.d.ts +24 -0
- package/lib/types/client/mcp-model.js +87 -0
- package/lib/types/client/mode-model.d.ts +108 -0
- package/lib/types/client/mode-model.js +140 -0
- package/lib/types/client/preset-display.d.ts +21 -0
- package/lib/types/client/preset-display.js +20 -0
- package/lib/types/client/settle.d.ts +40 -0
- package/lib/types/client/settle.js +57 -0
- package/lib/types/client/slots.d.ts +86 -0
- package/lib/types/client/slots.js +1 -0
- package/lib/types/envHint.d.ts +22 -0
- package/lib/types/envHint.js +76 -0
- package/lib/types/import.d.ts +106 -0
- package/lib/types/import.js +635 -0
- package/lib/types/index.d.ts +216 -0
- package/lib/types/index.js +739 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +23 -0
- package/lib/types/mcpConnector.d.ts +24 -0
- package/lib/types/mcpConnector.js +73 -0
- package/lib/types/mcpStore.d.ts +68 -0
- package/lib/types/mcpStore.js +235 -0
- package/lib/types/mcpTools.d.ts +45 -0
- package/lib/types/mcpTools.js +125 -0
- package/lib/types/mcps.d.ts +64 -0
- package/lib/types/mcps.js +157 -0
- package/lib/types/modes.d.ts +65 -0
- package/lib/types/modes.js +97 -0
- package/lib/types/namespace.d.ts +9 -0
- package/lib/types/namespace.js +8 -0
- package/lib/types/official-tools.d.ts +3 -0
- package/lib/types/official-tools.js +44 -0
- package/lib/types/preset-scope.d.ts +105 -0
- package/lib/types/preset-scope.js +117 -0
- package/lib/types/remote.d.ts +48 -0
- package/lib/types/remote.js +120 -0
- package/lib/types/scoped-delivery.d.ts +243 -0
- package/lib/types/scoped-delivery.js +686 -0
- package/lib/types/scoped-edits.d.ts +90 -0
- package/lib/types/scoped-edits.js +234 -0
- package/lib/types/settings.d.ts +28 -0
- package/lib/types/settings.js +28 -0
- package/lib/types/shellEnv.d.ts +65 -0
- package/lib/types/shellEnv.js +232 -0
- package/lib/types/skills.d.ts +103 -0
- package/lib/types/skills.js +303 -0
- package/lib/types/tool-origin.d.ts +29 -0
- package/lib/types/tool-origin.js +30 -0
- package/lib/types/types.d.ts +315 -0
- package/lib/types/types.js +8 -0
- package/lib/types/zip.d.ts +24 -0
- package/lib/types/zip.js +80 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +119 -0
- package/scripts/gen-official-tools.mts +52 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from 'react';
|
|
3
|
+
import { IconChevronDownOutlineMedium, IconChevronRightOutlineMedium, IconFolderCloseMedium, IconFolderOpenMedium } from './icons.js';
|
|
4
|
+
import css from './CapabilityCatalogCard.module.css';
|
|
5
|
+
/** Build a directory trie from flat bundle-relative paths. */
|
|
6
|
+
function buildBundleTree(files) {
|
|
7
|
+
const roots = [];
|
|
8
|
+
for (const file of files) {
|
|
9
|
+
const segments = file.split('/').filter(s => s !== '');
|
|
10
|
+
let level = roots;
|
|
11
|
+
let acc = '';
|
|
12
|
+
for (let i = 0; i < segments.length; i++) {
|
|
13
|
+
const seg = segments[i];
|
|
14
|
+
if (seg === undefined)
|
|
15
|
+
continue;
|
|
16
|
+
acc = acc === '' ? seg : `${acc}/${seg}`;
|
|
17
|
+
const last = i === segments.length - 1;
|
|
18
|
+
let node = level.find(n => n.name === seg);
|
|
19
|
+
if (node === undefined) {
|
|
20
|
+
node = { name: seg, path: acc, children: [], isDir: !last };
|
|
21
|
+
level.push(node);
|
|
22
|
+
}
|
|
23
|
+
level = node.children;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return [...roots].sort((a, b) => {
|
|
27
|
+
const aDir = a.isDir ? 0 : 1;
|
|
28
|
+
const bDir = b.isDir ? 0 : 1;
|
|
29
|
+
if (aDir !== bDir)
|
|
30
|
+
return aDir - bDir;
|
|
31
|
+
return a.name.localeCompare(b.name);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** A neutral document glyph for file rows (folded-corner page). */
|
|
35
|
+
function BundleFileGlyph() {
|
|
36
|
+
return (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", children: [_jsx("path", { d: "M9 1.5H4.5A1.5 1.5 0 0 0 3 3v10a1.5 1.5 0 0 0 1.5 1.5h7A1.5 1.5 0 0 0 13 13V5.5L9 1.5Z", stroke: "currentColor", strokeWidth: "1.2", strokeLinejoin: "round", fill: "none" }), _jsx("path", { d: "M9 1.5V5.5h4", stroke: "currentColor", strokeWidth: "1.2", strokeLinejoin: "round", fill: "none" })] }));
|
|
37
|
+
}
|
|
38
|
+
/** Skill-bundle file tree (IDE-explorer anatomy: chevron, folder, indent). */
|
|
39
|
+
export function BundleFileTree({ files, selectedPath, onSelect }) {
|
|
40
|
+
const [expanded, setExpanded] = useState(() => {
|
|
41
|
+
const all = new Set();
|
|
42
|
+
const walk = (nodes) => {
|
|
43
|
+
for (const node of nodes) {
|
|
44
|
+
if (node.isDir) {
|
|
45
|
+
all.add(node.path);
|
|
46
|
+
walk(node.children);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
walk(buildBundleTree(files));
|
|
51
|
+
return all;
|
|
52
|
+
});
|
|
53
|
+
const roots = useMemo(() => buildBundleTree(files), [files]);
|
|
54
|
+
const toggle = (path) => {
|
|
55
|
+
setExpanded(prev => {
|
|
56
|
+
const next = new Set(prev);
|
|
57
|
+
if (next.has(path))
|
|
58
|
+
next.delete(path);
|
|
59
|
+
else
|
|
60
|
+
next.add(path);
|
|
61
|
+
return next;
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
const renderNode = (node, depth) => {
|
|
65
|
+
const indent = { paddingLeft: `${depth * 16 + 4}px` };
|
|
66
|
+
if (node.isDir) {
|
|
67
|
+
const open = expanded.has(node.path);
|
|
68
|
+
return (_jsxs("div", { children: [_jsxs("button", { type: "button", className: css.treeRow, style: indent, onClick: () => toggle(node.path), children: [_jsx("span", { className: css.treeChevron, children: open ? _jsx(IconChevronDownOutlineMedium, { size: 16 }) : _jsx(IconChevronRightOutlineMedium, { size: 16 }) }), open ? _jsx(IconFolderOpenMedium, {}) : _jsx(IconFolderCloseMedium, {}), _jsx("span", { className: css.treeName, children: node.name })] }), open && node.children.length > 0 ? (_jsx("div", { children: node.children.map(child => renderNode(child, depth + 1)) })) : null] }, node.path));
|
|
69
|
+
}
|
|
70
|
+
const selected = selectedPath === node.path;
|
|
71
|
+
return (_jsxs("button", { type: "button", className: `${css.treeRow} ${css.treeFile} ${selected ? css.treeSelected : ''}`, style: indent, onClick: () => onSelect(node.path), children: [_jsx("span", { className: css.treeGlyph, children: _jsx(BundleFileGlyph, {}) }), _jsx("span", { className: css.treeName, children: node.name })] }, node.path));
|
|
72
|
+
};
|
|
73
|
+
return (_jsx("div", { className: css.tree, children: roots.map(node => renderNode(node, 0)) }));
|
|
74
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { CatalogPresetOption, CatalogPresetScopeStatus } from '@khorsheed/dsh-capability-catalog/types';
|
|
2
|
+
import type { CapabilityCatalogCardProps } from './slots.ts';
|
|
3
|
+
import { type ScopeEditor } from './SkillDetailModal.tsx';
|
|
4
|
+
export declare function CapabilityCatalogCard({ useCatalog, detail, readSkillFile, listDirSkills, pickDirectory, setCredential, addSkill, deleteSkill, refresh, refreshSettled, mcpSnapshot, mcpAdd, mcpRemove, mcpSetEnabled, mcpSetCredential, mcpSetToolEnabled, mcpDiscover, modeFaces, presetScopeStatus, presetScopeRoster, presetScopeSet, presetScopeAdopt, presetScopeRelease, t, }: CapabilityCatalogCardProps): import("react").JSX.Element;
|
|
5
|
+
/**
|
|
6
|
+
* Whether a deployment can write THIS skill's preset scope at all: it lives in
|
|
7
|
+
* the plugin's managed root, or in a default root the catalog may adopt into it.
|
|
8
|
+
*
|
|
9
|
+
* The same rule {@link scopeEditorFor} renders, as a boolean for callers that
|
|
10
|
+
* only need to know whether the modes are knowable from the scope editor — a
|
|
11
|
+
* caller asking "which modes load this skill?" should read the faces, not the
|
|
12
|
+
* declared scope, exactly when this is false.
|
|
13
|
+
*/
|
|
14
|
+
export declare function presetScopeWritable(name: string, skills: readonly {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly source: string;
|
|
17
|
+
}[], status: CatalogPresetScopeStatus | null): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* The scope editor for one skill, or undefined when the deployment has no
|
|
20
|
+
* delivery surface at all (no roster, no managed root).
|
|
21
|
+
*
|
|
22
|
+
* `writable` is the honest half of this face. The host writes a preset scope
|
|
23
|
+
* into the frontmatter of a skill in ITS OWN managed root (`setManagedPresetScope`
|
|
24
|
+
* answers `"<name>" is not a managed skill` for anything else), so exactly two
|
|
25
|
+
* shapes are configurable here: a skill already in that root, and one in a
|
|
26
|
+
* default root the catalog may adopt INTO it. A plugin-provided skill (source
|
|
27
|
+
* `runtime`) or a built-in one (source `bundled`) is neither: which modes see it
|
|
28
|
+
* is decided by the plugin's own row in each mode's composition, and offering
|
|
29
|
+
* the checkerboard with a Save button only produced a refusal. Those rows keep
|
|
30
|
+
* their section and get an explanation instead — `provider` names the plugin
|
|
31
|
+
* that decides.
|
|
32
|
+
* @param name - the skill whose detail modal is open.
|
|
33
|
+
* @param skills - the catalog rows, for the skill's own source.
|
|
34
|
+
* @param status - the delivery status, or null before it first resolved.
|
|
35
|
+
* @param options - the roster the picker offers.
|
|
36
|
+
* @param actions - the three write paths, already bound to this skill.
|
|
37
|
+
* @returns the editor face, or undefined when there is nothing to say.
|
|
38
|
+
*/
|
|
39
|
+
export declare function scopeEditorFor(name: string, skills: readonly {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
readonly source: string;
|
|
42
|
+
readonly provider?: string;
|
|
43
|
+
}[], status: CatalogPresetScopeStatus | null, options: readonly CatalogPresetOption[], actions: {
|
|
44
|
+
readonly save: (presets: readonly string[]) => Promise<{
|
|
45
|
+
ok: boolean;
|
|
46
|
+
error?: string;
|
|
47
|
+
}>;
|
|
48
|
+
readonly adopt: (presets: readonly string[]) => Promise<{
|
|
49
|
+
ok: boolean;
|
|
50
|
+
error?: string;
|
|
51
|
+
}>;
|
|
52
|
+
readonly release: () => Promise<{
|
|
53
|
+
ok: boolean;
|
|
54
|
+
error?: string;
|
|
55
|
+
}>;
|
|
56
|
+
}): ScopeEditor | undefined;
|
|
57
|
+
//# sourceMappingURL=CapabilityCatalogCard.d.ts.map
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Capability-catalog settings section (工具与技能): a standalone nav tab.
|
|
4
|
+
* Owns the section state and Remote wiring; visual cards and dialogs live in
|
|
5
|
+
* focused client modules alongside this shell.
|
|
6
|
+
*/
|
|
7
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
|
+
import { IconSearchOutlineMedium } from './icons.js';
|
|
9
|
+
import { SkillPreviewCard, DeleteSkillConfirm } from './SkillCards.js';
|
|
10
|
+
import { ToolCards } from './ToolCards.js';
|
|
11
|
+
import { ToolDetailModal } from './ToolDetailModal.js';
|
|
12
|
+
import { ModalShell } from './ModalShell.js';
|
|
13
|
+
import { SkillDetailModal } from './SkillDetailModal.js';
|
|
14
|
+
import { AddSkillModal } from './AddSkillModal.js';
|
|
15
|
+
import { McpServerManageModal } from './McpServerManageModal.js';
|
|
16
|
+
import { AddMcpDialog } from './AddMcpDialog.js';
|
|
17
|
+
import { buildMcpGroups } from './mcp-model.js';
|
|
18
|
+
import { buildModeComparison, orphanManagedSkills, resolveModeChips } from './mode-model.js';
|
|
19
|
+
import { presetName } from './preset-display.js';
|
|
20
|
+
import css from './CapabilityCatalogCard.module.css';
|
|
21
|
+
/** The mode control's value for the cross-mode comparison: never a preset id. */
|
|
22
|
+
const MODE_COMPARE = '\u0000compare';
|
|
23
|
+
/** Bucket a skill source into a segment (bundled→内置, runtime→插件, else→其他). */
|
|
24
|
+
const skillBucket = (source) => source === 'bundled' ? 'builtin' : source === 'runtime' ? 'plugin' : 'other';
|
|
25
|
+
export function CapabilityCatalogCard({ useCatalog, detail, readSkillFile, listDirSkills, pickDirectory, setCredential, addSkill, deleteSkill, refresh, refreshSettled, mcpSnapshot, mcpAdd, mcpRemove, mcpSetEnabled, mcpSetCredential, mcpSetToolEnabled, mcpDiscover, modeFaces, presetScopeStatus, presetScopeRoster, presetScopeSet, presetScopeAdopt, presetScopeRelease, t, }) {
|
|
26
|
+
const snapshot = useCatalog((s) => s);
|
|
27
|
+
const [kind, setKind] = useState('skills');
|
|
28
|
+
const [selectedName, setSelectedName] = useState(null);
|
|
29
|
+
const [claim, setClaim] = useState({ status: 'idle', data: undefined });
|
|
30
|
+
const [showAdd, setShowAdd] = useState(false);
|
|
31
|
+
const [showAddMcp, setShowAddMcp] = useState(false);
|
|
32
|
+
const [deleteTarget, setDeleteTarget] = useState(null);
|
|
33
|
+
// Grid filter / sort state.
|
|
34
|
+
const [query, setQuery] = useState('');
|
|
35
|
+
const [sortBy, setSortBy] = useState('name');
|
|
36
|
+
// Tool grid segment (builtin / plugin / mcp).
|
|
37
|
+
const [toolSegment, setToolSegment] = useState('all');
|
|
38
|
+
// Skill grid segment (builtin=bundled / plugin=runtime / other=project+user+custom).
|
|
39
|
+
const [skillSegment, setSkillSegment] = useState('all');
|
|
40
|
+
// Tool detail (click a tool card to view its full detail).
|
|
41
|
+
const [toolDetail, setToolDetail] = useState(null);
|
|
42
|
+
// MCP management state: the snapshot, the open add-dialog, expanded servers,
|
|
43
|
+
// and the set currently mid-discover.
|
|
44
|
+
const [mcps, setMcps] = useState(null);
|
|
45
|
+
const [mcpDetailName, setMcpDetailName] = useState(null);
|
|
46
|
+
const [discoveringMcp, setDiscoveringMcp] = useState(() => new Set());
|
|
47
|
+
// Tool-origin guidance modal ("why is my plugin tool not here").
|
|
48
|
+
const [toolOriginHelp, setToolOriginHelp] = useState(false);
|
|
49
|
+
// The modes that load the skill whose detail is open, when its scope section
|
|
50
|
+
// cannot say (see loadDetailModes). null = not queried / not applicable.
|
|
51
|
+
const [detailModes, setDetailModes] = useState(null);
|
|
52
|
+
// Preset-scoped delivery: the managed-root status and the roster the picker shows.
|
|
53
|
+
const [scopeStatus, setScopeStatus] = useState(null);
|
|
54
|
+
const [presetOptions, setPresetOptions] = useState([]);
|
|
55
|
+
// The MODE the grid shows (one preset's face, or every mode compared) and the
|
|
56
|
+
// comparison's faces. `null` mode means "not chosen yet": the store holds the
|
|
57
|
+
// deployment default's face, so the grid renders it while the roster loads.
|
|
58
|
+
const [mode, setMode] = useState(null);
|
|
59
|
+
const [faces, setFaces] = useState(null);
|
|
60
|
+
const [modeBusy, setModeBusy] = useState(false);
|
|
61
|
+
// The mode is mirrored in a ref because the mount effect and the mutation
|
|
62
|
+
// handlers re-read the view they were installed for, not the one at render.
|
|
63
|
+
const modeRef = useRef(null);
|
|
64
|
+
/** Roster rows with display names localized for the active locale: the four
|
|
65
|
+
* shipped presets publish no name (their copy resolves through the host's
|
|
66
|
+
* built-in-preset keys), so every surface renders these, never the raw rows.
|
|
67
|
+
* Identity fields (id/isDefault/broken) pass through untouched. */
|
|
68
|
+
const localizedOptions = useMemo(() => presetOptions.map(option => ({ ...option, name: presetName(option, t) })), [presetOptions, t]);
|
|
69
|
+
/** Mode faces with the same localization applied (their `name` feeds the
|
|
70
|
+
* comparison's mode labels and the unavailable list). */
|
|
71
|
+
const localizedFaces = useMemo(() => (faces === null ? null : faces.map(face => ({ ...face, name: presetName({ id: face.preset, name: face.name }, t) }))), [faces, t]);
|
|
72
|
+
/** The preset a session naming none composes, i.e. the mode the store holds
|
|
73
|
+
* before a human picks one — the store's first read is the default's face. */
|
|
74
|
+
const defaultPreset = useMemo(() => localizedOptions.find(option => option.isDefault === true) ?? localizedOptions[0], [localizedOptions]);
|
|
75
|
+
/** The preset id a read of THIS view should use (undefined = global fallback). */
|
|
76
|
+
const presetIdOf = useCallback((selection) => selection === null ? defaultPreset?.id : selection.kind === 'preset' ? selection.id : undefined, [defaultPreset]);
|
|
77
|
+
/** The comparison model, or null when the grid shows one mode's face. */
|
|
78
|
+
const comparison = useMemo(() => (mode?.kind === 'compare' && localizedFaces !== null ? buildModeComparison(localizedFaces) : null), [mode, localizedFaces]);
|
|
79
|
+
const comparing = mode?.kind === 'compare';
|
|
80
|
+
const faceSkills = comparison !== null ? comparison.skills : (snapshot?.skills ?? []);
|
|
81
|
+
/** The grid is the mode's UNFILTERED face — nothing is merged into it.
|
|
82
|
+
*
|
|
83
|
+
* The delivery work merged managed-root rows the current scope did not hold,
|
|
84
|
+
* so they stayed editable from one grid. With a mode control that merge is a
|
|
85
|
+
* lie: a skill scoped to 写作模式 appeared under 开发模式, and the tab's own
|
|
86
|
+
* count was the union rather than the mode's. A managed skill is reachable by
|
|
87
|
+
* selecting a mode that loads it (or the comparison), and `orphans` below
|
|
88
|
+
* keeps the one case no mode can show reachable. */
|
|
89
|
+
const skills = faceSkills;
|
|
90
|
+
/** Display name per preset id, for the managed badges. */
|
|
91
|
+
const presetNames = useMemo(() => new Map(localizedOptions.map(option => [option.id, option.name ?? option.id])), [localizedOptions]);
|
|
92
|
+
/** One chip per mode id a capability was found in (comparison view only). */
|
|
93
|
+
const modeChipsFor = useCallback((ids) => ids === undefined ? [] : resolveModeChips(ids, localizedOptions), [localizedOptions]);
|
|
94
|
+
/** The chips a card should render, or undefined when it has no row at all: an
|
|
95
|
+
* EMPTY list must not claim the row, because the card gives that row its bottom
|
|
96
|
+
* padding (see `.pvCard[data-modes]` in the stylesheet). */
|
|
97
|
+
const cardModeChips = useCallback((ids) => {
|
|
98
|
+
if (comparison === null)
|
|
99
|
+
return undefined;
|
|
100
|
+
const chips = modeChipsFor(ids);
|
|
101
|
+
return chips.length === 0 ? undefined : chips;
|
|
102
|
+
}, [comparison, modeChipsFor]);
|
|
103
|
+
/** A managed skill's preset-scope badge, for the surfaces where that policy is
|
|
104
|
+
* the point (the orphan list, the modal) — never the mode grid, where it would
|
|
105
|
+
* impersonate a source. */
|
|
106
|
+
const scopeTagOf = useCallback((name) => {
|
|
107
|
+
const row = scopeStatus?.skills.find(entry => entry.name === name);
|
|
108
|
+
if (row === undefined)
|
|
109
|
+
return undefined;
|
|
110
|
+
const names = row.presets.map(id => presetNames.get(id) ?? id);
|
|
111
|
+
return `${t('scopePresetTag')} · ${names.length === 0 ? t('scopeAllPresets') : names.join('、')}`;
|
|
112
|
+
}, [scopeStatus, presetNames, t]);
|
|
113
|
+
/** Managed skills no readable mode can show (see {@link orphanManagedSkills}). */
|
|
114
|
+
const orphans = useMemo(() => {
|
|
115
|
+
if (scopeStatus === null)
|
|
116
|
+
return [];
|
|
117
|
+
const failed = scopeStatus.presets.filter(row => row.error !== undefined).map(row => row.presetId);
|
|
118
|
+
return orphanManagedSkills(scopeStatus.skills, localizedOptions, failed);
|
|
119
|
+
}, [scopeStatus, localizedOptions]);
|
|
120
|
+
const tools = comparison !== null ? comparison.tools : (snapshot?.tools ?? []);
|
|
121
|
+
const loading = snapshot == null && comparison === null;
|
|
122
|
+
/** Enter a mode: the store holds one face at a time, so a preset switch is a
|
|
123
|
+
* re-read at that preset's scope and the comparison is a separate fetch. */
|
|
124
|
+
const selectMode = (next) => {
|
|
125
|
+
modeRef.current = next;
|
|
126
|
+
setMode(next);
|
|
127
|
+
if (next.kind === 'compare') {
|
|
128
|
+
void loadFaces();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
setModeBusy(true);
|
|
132
|
+
void refresh(next.id).finally(() => setModeBusy(false));
|
|
133
|
+
};
|
|
134
|
+
/** Read every mode's face (the host composes a mode nothing has mounted yet).
|
|
135
|
+
* `force` re-reads after a mutation; otherwise the shared cache answers. */
|
|
136
|
+
const loadFaces = async (force = false) => {
|
|
137
|
+
setModeBusy(true);
|
|
138
|
+
try {
|
|
139
|
+
setFaces(await modeFaces(force));
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
setModeBusy(false);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
/** Re-read whatever the grid is showing: one mode's face, or every face. */
|
|
146
|
+
const reloadView = async () => {
|
|
147
|
+
if (modeRef.current?.kind === 'compare')
|
|
148
|
+
await loadFaces(true);
|
|
149
|
+
else
|
|
150
|
+
await refresh(presetIdOf(modeRef.current));
|
|
151
|
+
};
|
|
152
|
+
/** Re-fetch the catalog + MCP snapshots after any MCP mutation. MCP
|
|
153
|
+
* add/remove/enable/discover changes which `mcp__` tools are registered on the
|
|
154
|
+
* host tool registry, so the catalog grid must refresh too — not just the MCP
|
|
155
|
+
* store — or a removed server's tools linger until a manual page refresh. */
|
|
156
|
+
const refreshMcp = async () => {
|
|
157
|
+
await Promise.all([
|
|
158
|
+
reloadView(),
|
|
159
|
+
mcpSnapshot().then(setMcps),
|
|
160
|
+
]);
|
|
161
|
+
};
|
|
162
|
+
/** Re-read the preset-scope status and roster after any scope write. */
|
|
163
|
+
const refreshScope = async () => {
|
|
164
|
+
const [status, roster] = await Promise.all([presetScopeStatus(), presetScopeRoster()]);
|
|
165
|
+
setScopeStatus(status ?? null);
|
|
166
|
+
setPresetOptions(roster);
|
|
167
|
+
return roster;
|
|
168
|
+
};
|
|
169
|
+
useEffect(() => { void refreshMcp(); }, []);
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
// The roster lands after the first read; adopt its default as the initial
|
|
172
|
+
// mode WITHOUT re-reading (the store already holds that face).
|
|
173
|
+
void refreshScope().then((roster) => {
|
|
174
|
+
const initial = roster.find(option => option.isDefault === true) ?? roster[0];
|
|
175
|
+
if (initial === undefined)
|
|
176
|
+
return;
|
|
177
|
+
const next = { kind: 'preset', id: initial.id };
|
|
178
|
+
modeRef.current = next;
|
|
179
|
+
setMode(next);
|
|
180
|
+
});
|
|
181
|
+
}, []);
|
|
182
|
+
const visibleSkills = useMemo(() => {
|
|
183
|
+
const q = query.trim().toLowerCase();
|
|
184
|
+
const matched = skills.filter((s) => (q === '' || s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q))
|
|
185
|
+
&& (skillSegment === 'all' || skillBucket(s.source) === skillSegment));
|
|
186
|
+
const sorted = [...matched];
|
|
187
|
+
if (sortBy === 'updated')
|
|
188
|
+
sorted.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
|
189
|
+
else
|
|
190
|
+
sorted.sort((a, b) => a.name.localeCompare(b.name));
|
|
191
|
+
return sorted;
|
|
192
|
+
}, [skills, query, skillSegment, sortBy]);
|
|
193
|
+
/** Per-segment counts for the skills grid. */
|
|
194
|
+
const skillBuiltinCount = useMemo(() => skills.filter((s) => skillBucket(s.source) === 'builtin').length, [skills]);
|
|
195
|
+
const skillPluginCount = useMemo(() => skills.filter((s) => skillBucket(s.source) === 'plugin').length, [skills]);
|
|
196
|
+
const skillOtherCount = useMemo(() => skills.filter((s) => skillBucket(s.source) === 'other').length, [skills]);
|
|
197
|
+
/** Per-segment counts for the three grid filters. */
|
|
198
|
+
const builtinCount = useMemo(() => tools.filter((tool) => tool.channel === 'builtin').length, [tools]);
|
|
199
|
+
const pluginCount = useMemo(() => tools.filter((tool) => tool.channel === 'plugin').length, [tools]);
|
|
200
|
+
/** Tool cards for the current segment. `all` shows builtin + plugin (MCP still
|
|
201
|
+
* renders as server cards below). Filtered by query, name-sorted. */
|
|
202
|
+
const visibleTools = useMemo(() => {
|
|
203
|
+
const q = query.trim().toLowerCase();
|
|
204
|
+
const matched = tools.filter((tool) => {
|
|
205
|
+
if (toolSegment === 'all') {
|
|
206
|
+
if (tool.channel !== 'builtin' && tool.channel !== 'plugin')
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
else if (tool.channel !== toolSegment) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
if (q === '')
|
|
213
|
+
return true;
|
|
214
|
+
return tool.name.toLowerCase().includes(q)
|
|
215
|
+
|| tool.description.toLowerCase().includes(q)
|
|
216
|
+
|| (tool.serverName ?? '').toLowerCase().includes(q)
|
|
217
|
+
|| (tool.owner ?? '').toLowerCase().includes(q);
|
|
218
|
+
});
|
|
219
|
+
return matched.sort((a, b) => a.name.localeCompare(b.name));
|
|
220
|
+
}, [tools, query, toolSegment]);
|
|
221
|
+
/** Merged MCP server groups for the `mcp` segment: catalog-managed servers
|
|
222
|
+
* + any live-registered (mcp-<server>__<tool>) tools grouped by server. The
|
|
223
|
+
* comparison view feeds it the UNION of every mode's tools, so a server only
|
|
224
|
+
* some modes see still appears. */
|
|
225
|
+
const mcpGroups = useMemo(() => buildMcpGroups(comparison === null ? snapshot : { skills: [], tools, mcpServers: [], channels: [] }, mcps, query), [comparison, snapshot, tools, mcps, query]);
|
|
226
|
+
/** The MCP server group open in the manage modal (derived fresh so mutations
|
|
227
|
+
* re-render it, never a stale snapshot), or null when closed. */
|
|
228
|
+
const mcpDetail = useMemo(() => mcpGroups.find((g) => g.serverName === mcpDetailName) ?? null, [mcpGroups, mcpDetailName]);
|
|
229
|
+
const removeMcp = async (serverName) => {
|
|
230
|
+
await mcpRemove(serverName);
|
|
231
|
+
await refreshMcp();
|
|
232
|
+
};
|
|
233
|
+
const setMcpEnabled = async (serverName, enabled) => {
|
|
234
|
+
await mcpSetEnabled(serverName, enabled);
|
|
235
|
+
await refreshMcp();
|
|
236
|
+
};
|
|
237
|
+
const setMcpToolEnabled = async (serverName, tool, enabled) => {
|
|
238
|
+
await mcpSetToolEnabled(serverName, tool, enabled);
|
|
239
|
+
await refreshMcp();
|
|
240
|
+
};
|
|
241
|
+
const discoverMcp = async (serverName) => {
|
|
242
|
+
setDiscoveringMcp((prev) => new Set(prev).add(serverName));
|
|
243
|
+
try {
|
|
244
|
+
await mcpDiscover(serverName);
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
setDiscoveringMcp((prev) => { const n = new Set(prev); n.delete(serverName); return n; });
|
|
248
|
+
await refreshMcp();
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* The mode a READ of one capability should use. A preset view reads its own
|
|
253
|
+
* mode. The comparison has no single mode, so it reads where the capability
|
|
254
|
+
* actually lives — the default mode when that mode has it, else the first
|
|
255
|
+
* mode that does — and never a mode that does not register it at all.
|
|
256
|
+
*/
|
|
257
|
+
const readModeFor = (name) => {
|
|
258
|
+
const current = modeRef.current;
|
|
259
|
+
if (current === null || current.kind === 'preset')
|
|
260
|
+
return current?.id;
|
|
261
|
+
const modes = comparison?.skillModes.get(name) ?? [];
|
|
262
|
+
if (defaultPreset !== undefined && modes.includes(defaultPreset.id))
|
|
263
|
+
return defaultPreset.id;
|
|
264
|
+
return modes[0] ?? defaultPreset?.id;
|
|
265
|
+
};
|
|
266
|
+
/** The mode control's current value. */
|
|
267
|
+
const modeValue = mode?.kind === 'compare' ? MODE_COMPARE : (presetIdOf(mode) ?? '');
|
|
268
|
+
const handleModeChange = (value) => {
|
|
269
|
+
selectMode(value === MODE_COMPARE ? { kind: 'compare' } : { kind: 'preset', id: value });
|
|
270
|
+
};
|
|
271
|
+
/** Whether a selected mode fell back to the global layer (its read carries no
|
|
272
|
+
* `preset` stamp), i.e. the preset could not compose — the grid is NOT that
|
|
273
|
+
* mode's face and must say so. */
|
|
274
|
+
const modeFellBack = !comparing
|
|
275
|
+
&& mode !== null
|
|
276
|
+
&& snapshot !== undefined
|
|
277
|
+
&& presetIdOf(mode) !== undefined
|
|
278
|
+
&& snapshot.preset !== presetIdOf(mode);
|
|
279
|
+
/**
|
|
280
|
+
* The modes that LOAD one skill, for a detail modal whose scope section
|
|
281
|
+
* cannot tell: a plugin-provided or built-in skill has no preset editor, so
|
|
282
|
+
* the modes that carry it are the only answer to "where does it apply".
|
|
283
|
+
*
|
|
284
|
+
* Read from the shared faces cache, so a panel that already compared modes
|
|
285
|
+
* answers instantly and a first-time open pays for the composition once. The
|
|
286
|
+
* question is asked only where it is otherwise unanswerable: a skill whose
|
|
287
|
+
* scope IS editable shows its declared scope instead.
|
|
288
|
+
*/
|
|
289
|
+
const loadDetailModes = async (name) => {
|
|
290
|
+
if (presetScopeWritable(name, skills, scopeStatus)) {
|
|
291
|
+
setDetailModes(null);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
setDetailModes({ loading: true, ids: [] });
|
|
295
|
+
const all = await modeFaces();
|
|
296
|
+
setDetailModes({
|
|
297
|
+
loading: false,
|
|
298
|
+
ids: all
|
|
299
|
+
.filter(face => face.unavailable === undefined && face.skills.some(skill => skill.name === name))
|
|
300
|
+
.map(face => face.preset),
|
|
301
|
+
});
|
|
302
|
+
};
|
|
303
|
+
const openDetail = async (name) => {
|
|
304
|
+
setSelectedName(name);
|
|
305
|
+
setClaim({ status: 'loading', data: undefined });
|
|
306
|
+
void loadDetailModes(name);
|
|
307
|
+
const data = await detail(name, readModeFor(name));
|
|
308
|
+
setClaim({ status: 'done', data });
|
|
309
|
+
};
|
|
310
|
+
const closeDetail = () => {
|
|
311
|
+
setSelectedName(null);
|
|
312
|
+
setClaim({ status: 'idle', data: undefined });
|
|
313
|
+
setDetailModes(null);
|
|
314
|
+
};
|
|
315
|
+
return (_jsxs("div", { className: css.section, children: [_jsx("div", { className: css.headRow, children: _jsx("h2", { className: css.heading, children: t('title') }) }), _jsx("p", { className: css.intro, children: t('intro') }), _jsxs("div", { className: css.toolbar, children: [_jsxs("div", { className: css.tabs, role: "tablist", children: [_jsxs("button", { type: "button", className: css.tab, "data-active": kind === 'skills', role: "tab", onClick: () => setKind('skills'), children: [t('skillTab'), _jsx("span", { className: css.tabCnt, children: skills.length })] }), _jsxs("button", { type: "button", className: css.tab, "data-active": kind === 'tools', role: "tab", onClick: () => setKind('tools'), children: [t('toolTab'), _jsx("span", { className: css.tabCnt, children: tools.length })] })] }), _jsx("button", { type: "button", className: css.addBtn, onClick: () => (kind === 'skills' ? setShowAdd(true) : setShowAddMcp(true)), children: kind === 'skills' ? t('addSkill') : t('addMcp') })] }), !loading && kind === 'skills' && (skills.length > 0 || presetOptions.length > 0) ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.filterBar, role: "search", children: [_jsxs("div", { className: css.searchBox, children: [_jsx("span", { className: css.searchIcon, children: _jsx(IconSearchOutlineMedium, { size: 16 }) }), _jsx("input", { className: css.searchInput, type: "search", value: query, placeholder: t('searchPlaceholder'), "aria-label": t('searchPlaceholder'), onChange: (e) => setQuery(e.target.value) })] }), _jsxs("select", { className: css.select, value: sortBy, "aria-label": t('sortBy'), onChange: (e) => setSortBy(e.target.value), children: [_jsxs("option", { value: "name", children: [t('sortBy'), ": ", t('sortName')] }), _jsxs("option", { value: "updated", children: [t('sortBy'), ": ", t('sortUpdated')] })] }), _jsx(ModeSelect, { options: localizedOptions, value: modeValue, busy: modeBusy, onSelect: handleModeChange, t: t })] }), _jsx(SegmentBar, { segments: [
|
|
316
|
+
{ id: 'all', label: t('filterAll'), count: skills.length },
|
|
317
|
+
{ id: 'builtin', label: t('builtin'), count: skillBuiltinCount },
|
|
318
|
+
{ id: 'plugin', label: t('toolPlugin'), count: skillPluginCount },
|
|
319
|
+
{ id: 'other', label: t('skillOther'), count: skillOtherCount },
|
|
320
|
+
], active: skillSegment, onSelect: (id) => setSkillSegment(id), t: t })] })) : null, !loading && kind === 'tools' && (tools.length > 0 || presetOptions.length > 0) ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.filterBar, role: "search", children: [_jsxs("div", { className: css.searchBox, children: [_jsx("span", { className: css.searchIcon, children: _jsx(IconSearchOutlineMedium, { size: 16 }) }), _jsx("input", { className: css.searchInput, type: "search", value: query, placeholder: t('toolSearchPlaceholder'), "aria-label": t('toolSearchPlaceholder'), onChange: (e) => setQuery(e.target.value) })] }), _jsx(ModeSelect, { options: localizedOptions, value: modeValue, busy: modeBusy, onSelect: handleModeChange, t: t })] }), _jsx(SegmentBar, { segments: [
|
|
321
|
+
{ id: 'all', label: t('filterAll'), count: tools.length },
|
|
322
|
+
{ id: 'builtin', label: t('toolBuiltin'), count: builtinCount },
|
|
323
|
+
{ id: 'plugin', label: t('toolPlugin'), count: pluginCount },
|
|
324
|
+
{ id: 'mcp', label: t('toolOther'), count: mcpGroups.length },
|
|
325
|
+
], active: toolSegment, onSelect: (id) => setToolSegment(id), t: t }), toolSegment === 'plugin' ? (_jsx("button", { type: "button", className: css.guideLink, onClick: () => setToolOriginHelp(true), children: t('toolOriginGuideLink') })) : null] })) : null, comparing ? (_jsxs("div", { className: css.modeNote, "aria-busy": modeBusy, children: [modeBusy
|
|
326
|
+
? t('modeBusy')
|
|
327
|
+
: t('modeCompareNote')
|
|
328
|
+
.replace('{n}', String(comparison?.modes ?? 0))
|
|
329
|
+
.replace('{skills}', String(skills.length))
|
|
330
|
+
.replace('{tools}', String(tools.length)), !modeBusy && comparison !== null && comparison.unavailable.length > 0 ? (_jsx("span", { className: css.modeWarn, title: comparison.unavailable.map(entry => `${entry.label}: ${entry.reason}`).join('\n'), children: t('modeUnavailableNote').replace('{n}', String(comparison.unavailable.length)) })) : null, !modeBusy ? _jsx("span", { className: css.modeHint, children: t('modeCompareCost') }) : null] })) : null, !comparing && modeBusy ? _jsx("div", { className: css.modeNote, children: t('modeBusy') }) : null, modeFellBack ? _jsx("div", { className: css.modeNote, children: t('modeFallback') }) : null, loading ? _jsx("div", { className: css.empty, children: t('loading') }) : null, !loading && kind === 'skills' && skills.length === 0 ? _jsx("div", { className: css.empty, children: t('empty') }) : null, !loading && kind === 'tools' && tools.length === 0 ? _jsx("div", { className: css.empty, children: t('toolNoMatch') }) : null, !loading && kind === 'skills' && skills.length > 0 ? (visibleSkills.length === 0
|
|
331
|
+
? _jsx("div", { className: css.empty, children: t('noFilterMatch') })
|
|
332
|
+
: (_jsx("div", { className: css.grid, children: visibleSkills.map((skill) => (_jsx(SkillPreviewCard, { skill: skill, modes: cardModeChips(comparison?.skillModes.get(skill.name)), modeTotal: comparison?.modes ?? 0, onMode: handleModeChange, onOpen: () => void openDetail(skill.name), onDelete: () => setDeleteTarget(skill.name), t: t }, skill.name))) }))) : null, !loading && kind === 'skills' && orphans.length > 0 ? (_jsxs("details", { className: css.orphanGroup, children: [_jsx("summary", { className: css.orphanSummary, children: t('orphanManaged').replace('{n}', String(orphans.length)) }), _jsx("p", { className: css.confHint, children: t('orphanManagedHint') }), _jsx("div", { className: css.grid, children: orphans.map(orphan => (_jsx(SkillPreviewCard, { skill: orphanSkillRow(orphan), tag: scopeTagOf(orphan.name), onMode: handleModeChange, onOpen: () => void openDetail(orphan.name), onDelete: () => setDeleteTarget(orphan.name), t: t }, orphan.name))) })] })) : null, kind === 'tools' ? (_jsx(ToolCards, { loading: loading, visibleTools: visibleTools, mcpGroups: mcpGroups, segment: toolSegment, comparison: comparison, modeChipsFor: modeChipsFor, onMode: handleModeChange, onOpenTool: (tool) => setToolDetail(tool), onOpenServer: (name) => setMcpDetailName(name), onSetEnabled: setMcpEnabled, onRemove: removeMcp, t: t })) : null, mcpDetail !== null ? (_jsx(McpServerManageModal, { group: mcpDetail, discovering: discoveringMcp.has(mcpDetail.serverName), onClose: () => setMcpDetailName(null), onSetCredential: mcpSetCredential, onSetToolEnabled: setMcpToolEnabled, onDiscover: discoverMcp, onOpenTool: (tool) => setToolDetail(tool), t: t })) : null, toolDetail !== null ? (_jsx(ToolDetailModal, { tool: toolDetail, onClose: () => setToolDetail(null), t: t })) : null, selectedName !== null ? (_jsx(SkillDetailModal, { name: selectedName, claim: claim, onClose: closeDetail, setCredential: setCredential, readSkillFile: readSkillFile, t: t, scope: scopeEditorFor(selectedName, skills, scopeStatus, localizedOptions, {
|
|
333
|
+
save: async (presets) => {
|
|
334
|
+
const result = await presetScopeSet(selectedName, presets);
|
|
335
|
+
await Promise.all([refreshScope(), reloadView()]);
|
|
336
|
+
return result;
|
|
337
|
+
},
|
|
338
|
+
adopt: async (presets) => {
|
|
339
|
+
const result = await presetScopeAdopt(selectedName, presets);
|
|
340
|
+
await Promise.all([refreshScope(), reloadView()]);
|
|
341
|
+
return result;
|
|
342
|
+
},
|
|
343
|
+
release: async () => {
|
|
344
|
+
const result = await presetScopeRelease(selectedName);
|
|
345
|
+
await Promise.all([refreshScope(), reloadView()]);
|
|
346
|
+
return result;
|
|
347
|
+
},
|
|
348
|
+
}), modeChips: detailModes === null || detailModes.loading ? undefined : modeChipsFor(detailModes.ids), modesLoading: detailModes?.loading === true,
|
|
349
|
+
// Switching modes replaces the grid the modal is describing, so the
|
|
350
|
+
// modal closes with the jump rather than outliving its subject.
|
|
351
|
+
onModeSelect: (id) => { closeDetail(); handleModeChange(id); } })) : null, showAdd ? (_jsx(AddSkillModal, { onClose: () => setShowAdd(false), addSkill: addSkill, listDirSkills: listDirSkills, pickDirectory: pickDirectory, refreshSettled: refreshSettled, t: t })) : null, showAddMcp ? (_jsx(AddMcpDialog, { onClose: () => setShowAddMcp(false), mcpAdd: mcpAdd, mcpSetCredential: mcpSetCredential, mcpDiscover: mcpDiscover, refreshMcp: refreshMcp, t: t })) : null, deleteTarget !== null ? (_jsx(DeleteSkillConfirm, { name: deleteTarget, onCancel: () => setDeleteTarget(null), onConfirm: async () => {
|
|
352
|
+
// Deleting reads the file at the mode the card was opened from: the
|
|
353
|
+
// same skill name can resolve to a different bundle in another mode.
|
|
354
|
+
const targetMode = readModeFor(deleteTarget);
|
|
355
|
+
const res = await deleteSkill(deleteTarget, targetMode);
|
|
356
|
+
// The removal reaches the registry through the host's watcher after
|
|
357
|
+
// the delete returns, so wait for the row to actually leave.
|
|
358
|
+
if (res.ok)
|
|
359
|
+
await refreshSettled(s => !s.skills.some(skill => skill.name === deleteTarget), targetMode);
|
|
360
|
+
setDeleteTarget(null);
|
|
361
|
+
}, t: t })) : null, toolOriginHelp ? _jsx(ToolOriginGuideModal, { onClose: () => setToolOriginHelp(false), t: t }) : null] }));
|
|
362
|
+
}
|
|
363
|
+
function SegmentBar({ segments, active, onSelect, t }) {
|
|
364
|
+
return (_jsx("div", { className: css.segBar, role: "group", "aria-label": t('source'), children: segments.map((s) => (_jsxs("button", { type: "button", className: css.segBtn, "data-active": active === s.id, onClick: () => onSelect(s.id), children: [s.label, _jsx("span", { className: css.tabCnt, children: s.count })] }, s.id))) }));
|
|
365
|
+
}
|
|
366
|
+
/** One mode option's label: its published name, plus the two facts a picker must
|
|
367
|
+
* not hide — it is the deployment default, or it cannot compose a session. */
|
|
368
|
+
function modeOptionLabel(option, t) {
|
|
369
|
+
const name = option.name ?? option.id;
|
|
370
|
+
if (option.broken !== undefined)
|
|
371
|
+
return `${name}(${t('scopeBroken')})`;
|
|
372
|
+
return option.isDefault === true ? `${name}(${t('modeDefault')})` : name;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* The mode control (`模式`): the grid's read position, beside the search and
|
|
376
|
+
* sort controls. One option per agent preset plus the comparison, which is how
|
|
377
|
+
* "which tools/skills does THIS mode load" and "which modes load THIS
|
|
378
|
+
* capability" are both answered from one grid.
|
|
379
|
+
*
|
|
380
|
+
* A vanished roster (no agent-preset service) renders nothing: a mode picker
|
|
381
|
+
* with no modes is noise, and the grid already reads the deployment default.
|
|
382
|
+
*/
|
|
383
|
+
function ModeSelect({ options, value, busy, onSelect, t }) {
|
|
384
|
+
if (options.length === 0)
|
|
385
|
+
return null;
|
|
386
|
+
return (_jsxs("select", { className: css.select, value: value, disabled: busy, "aria-label": t('modeLabel'), title: t('modeHint'), onChange: (e) => onSelect(e.target.value), children: [options.map(option => (_jsx("option", { value: option.id, children: modeOptionLabel(option, t) }, option.id))), _jsx("option", { value: MODE_COMPARE, children: t('modeAll') })] }));
|
|
387
|
+
}
|
|
388
|
+
/** Tool-origin guidance modal: explains why a plugin tool may show as builtin and
|
|
389
|
+
* copies a ready-to-send instruction (referencing the convention doc) for the
|
|
390
|
+
* user's agent to read and apply. */ function ToolOriginGuideModal({ onClose, t }) {
|
|
391
|
+
const [copied, setCopied] = useState(false);
|
|
392
|
+
const copyDoc = async () => {
|
|
393
|
+
try {
|
|
394
|
+
await navigator.clipboard.writeText(t('toolOriginGuideCopy'));
|
|
395
|
+
setCopied(true);
|
|
396
|
+
setTimeout(() => setCopied(false), 1200);
|
|
397
|
+
}
|
|
398
|
+
catch { /* clipboard may be blocked */ }
|
|
399
|
+
};
|
|
400
|
+
return (_jsxs(ModalShell, { title: t('toolOriginGuideTitle'), onClose: onClose, t: t, children: [_jsx("div", { className: css.confHint, children: t('toolOriginGuideIntro') }), _jsx("code", { className: css.guideDocPath, children: t('toolOriginGuideCopy') }), _jsxs("div", { className: css.guideFooter, children: [_jsx("span", { className: css.confHint, children: t('toolOriginGuideFooter') }), _jsx("button", { type: "button", className: css.btnPrimary, onClick: () => void copyDoc(), children: copied ? t('copied') : t('copy') })] })] }));
|
|
401
|
+
}
|
|
402
|
+
/** Sources the catalog may move out of a default root into the managed root. */
|
|
403
|
+
/** Source label for a managed skill rendered outside the mode that delivers it.
|
|
404
|
+
* Not a member of the deletable set: removal goes through `release`. */
|
|
405
|
+
const MANAGED_SOURCE = 'capability-catalog';
|
|
406
|
+
/** One orphan row as a card can render it. */
|
|
407
|
+
function orphanSkillRow(orphan) {
|
|
408
|
+
return {
|
|
409
|
+
name: orphan.name,
|
|
410
|
+
description: orphan.description,
|
|
411
|
+
source: MANAGED_SOURCE,
|
|
412
|
+
provider: 'capability-catalog',
|
|
413
|
+
modelInvocable: orphan.modelInvocable,
|
|
414
|
+
userInvocable: orphan.userInvocable,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/** Sources the catalog may move out of a default root into the managed root. */
|
|
418
|
+
const ADOPTABLE_SOURCES = new Set([
|
|
419
|
+
'user-dsh', 'user-agents', 'project-dsh', 'project-agents', 'custom',
|
|
420
|
+
]);
|
|
421
|
+
/**
|
|
422
|
+
* Whether a deployment can write THIS skill's preset scope at all: it lives in
|
|
423
|
+
* the plugin's managed root, or in a default root the catalog may adopt into it.
|
|
424
|
+
*
|
|
425
|
+
* The same rule {@link scopeEditorFor} renders, as a boolean for callers that
|
|
426
|
+
* only need to know whether the modes are knowable from the scope editor — a
|
|
427
|
+
* caller asking "which modes load this skill?" should read the faces, not the
|
|
428
|
+
* declared scope, exactly when this is false.
|
|
429
|
+
*/
|
|
430
|
+
export function presetScopeWritable(name, skills, status) {
|
|
431
|
+
if (status === null)
|
|
432
|
+
return false;
|
|
433
|
+
if (status.skills.some(row => row.name === name))
|
|
434
|
+
return true;
|
|
435
|
+
const row = skills.find(entry => entry.name === name);
|
|
436
|
+
return row !== undefined && ADOPTABLE_SOURCES.has(row.source);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* The scope editor for one skill, or undefined when the deployment has no
|
|
440
|
+
* delivery surface at all (no roster, no managed root).
|
|
441
|
+
*
|
|
442
|
+
* `writable` is the honest half of this face. The host writes a preset scope
|
|
443
|
+
* into the frontmatter of a skill in ITS OWN managed root (`setManagedPresetScope`
|
|
444
|
+
* answers `"<name>" is not a managed skill` for anything else), so exactly two
|
|
445
|
+
* shapes are configurable here: a skill already in that root, and one in a
|
|
446
|
+
* default root the catalog may adopt INTO it. A plugin-provided skill (source
|
|
447
|
+
* `runtime`) or a built-in one (source `bundled`) is neither: which modes see it
|
|
448
|
+
* is decided by the plugin's own row in each mode's composition, and offering
|
|
449
|
+
* the checkerboard with a Save button only produced a refusal. Those rows keep
|
|
450
|
+
* their section and get an explanation instead — `provider` names the plugin
|
|
451
|
+
* that decides.
|
|
452
|
+
* @param name - the skill whose detail modal is open.
|
|
453
|
+
* @param skills - the catalog rows, for the skill's own source.
|
|
454
|
+
* @param status - the delivery status, or null before it first resolved.
|
|
455
|
+
* @param options - the roster the picker offers.
|
|
456
|
+
* @param actions - the three write paths, already bound to this skill.
|
|
457
|
+
* @returns the editor face, or undefined when there is nothing to say.
|
|
458
|
+
*/
|
|
459
|
+
export function scopeEditorFor(name, skills, status, options, actions) {
|
|
460
|
+
if (status === null)
|
|
461
|
+
return undefined;
|
|
462
|
+
const managedRow = status.skills.find(row => row.name === name);
|
|
463
|
+
const catalogRow = skills.find(row => row.name === name);
|
|
464
|
+
if (managedRow === undefined && catalogRow === undefined)
|
|
465
|
+
return undefined;
|
|
466
|
+
const adoptable = managedRow === undefined && catalogRow !== undefined && ADOPTABLE_SOURCES.has(catalogRow.source);
|
|
467
|
+
return {
|
|
468
|
+
managed: managedRow !== undefined,
|
|
469
|
+
adoptable,
|
|
470
|
+
writable: managedRow !== undefined || adoptable,
|
|
471
|
+
...catalogRow?.provider === undefined ? {} : { provider: catalogRow.provider },
|
|
472
|
+
declared: managedRow?.presets ?? [],
|
|
473
|
+
...managedRow?.conflict === undefined ? {} : { conflict: managedRow.conflict },
|
|
474
|
+
options,
|
|
475
|
+
save: actions.save,
|
|
476
|
+
adopt: actions.adopt,
|
|
477
|
+
release: actions.release,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CapabilityCatalogKey } from './locales.ts';
|
|
2
|
+
export type CredentialSaveState = 'idle' | 'saving' | 'ok' | 'fail';
|
|
3
|
+
/** One presentational saved-credential row; state and save behavior stay local
|
|
4
|
+
* to the containing modal. */
|
|
5
|
+
export declare function CredentialField({ label, configured, value, state, onChange, onSave, t }: {
|
|
6
|
+
label: string;
|
|
7
|
+
configured: boolean;
|
|
8
|
+
value: string;
|
|
9
|
+
state: CredentialSaveState;
|
|
10
|
+
onChange: (value: string) => void;
|
|
11
|
+
onSave: () => void;
|
|
12
|
+
t: (key: CapabilityCatalogKey) => string;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
14
|
+
//# sourceMappingURL=CredentialField.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import css from './CapabilityCatalogCard.module.css';
|
|
3
|
+
/** One presentational saved-credential row; state and save behavior stay local
|
|
4
|
+
* to the containing modal. */
|
|
5
|
+
export function CredentialField({ label, configured, value, state, onChange, onSave, t }) {
|
|
6
|
+
return (_jsxs("div", { className: css.credRow, children: [_jsxs("label", { className: css.credLabel, children: [label, configured ? _jsx("span", { className: `${css.badge} ${css.badgeOk}`, children: t('configured') }) : _jsx("span", { className: css.badge, children: t('notConfigured') })] }), _jsxs("div", { className: css.credInputRow, children: [_jsx("div", { className: css.inputWrap, children: _jsx("input", { className: css.input, type: "password", value: value, placeholder: configured ? t('configuredReplace') : t('credPlaceholder'), onChange: (e) => onChange(e.target.value), onBlur: () => { if (value !== '')
|
|
7
|
+
onSave(); }, onKeyDown: (e) => { if (e.key === 'Enter')
|
|
8
|
+
onSave(); } }) }), !configured ? (_jsx("button", { type: "button", className: css.btnPrimary, disabled: state === 'saving' || value === '', onClick: onSave, children: t('save') })) : null] }), state === 'ok' ? _jsx("div", { className: css.credOk, children: t('saved') }) : null, state === 'fail' ? _jsx("div", { className: css.credFail, children: t('saveFailed') }) : null] }));
|
|
9
|
+
}
|