@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,167 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { IconChevronDownOutlineMedium, IconChevronRightOutlineMedium, IconCopyOutlineMedium } from './icons.js';
|
|
4
|
+
import { BundleFileTree } from './BundleFileTree.js';
|
|
5
|
+
import { CredentialField } from './CredentialField.js';
|
|
6
|
+
import { MetadataRow } from './MetadataRow.js';
|
|
7
|
+
import { ModalShell } from './ModalShell.js';
|
|
8
|
+
import css from './CapabilityCatalogCard.module.css';
|
|
9
|
+
/** Why a skill with no writable preset scope has no editor. */
|
|
10
|
+
function providedScopeNote(t, provider) {
|
|
11
|
+
return provider === undefined
|
|
12
|
+
? t('scopeBuiltin')
|
|
13
|
+
: t('scopeProvided').replace('{provider}', provider);
|
|
14
|
+
}
|
|
15
|
+
/** Centered modal with a skill's full detail, source browser, metadata and credential config. */
|
|
16
|
+
export function SkillDetailModal({ name, claim, onClose, setCredential, readSkillFile, t, scope, modeChips, modesLoading, onModeSelect }) {
|
|
17
|
+
const [credValues, setCredValues] = useState({});
|
|
18
|
+
const [credState, setCredState] = useState({});
|
|
19
|
+
const [copied, setCopied] = useState(false);
|
|
20
|
+
const [sourceOpen, setSourceOpen] = useState(false);
|
|
21
|
+
const data = claim.data;
|
|
22
|
+
// Source browser: the selected bundle file and its content (right pane).
|
|
23
|
+
const [srcFile, setSrcFile] = useState('');
|
|
24
|
+
const [srcContent, setSrcContent] = useState(undefined);
|
|
25
|
+
const [srcLoading, setSrcLoading] = useState(false);
|
|
26
|
+
// Preset-scope editor: the pending selection, the busy flag, and the last message.
|
|
27
|
+
const [scopePick, setScopePick] = useState(null);
|
|
28
|
+
const [scopeBusy, setScopeBusy] = useState(false);
|
|
29
|
+
const [scopeMsg, setScopeMsg] = useState(null);
|
|
30
|
+
const saveCred = async (key) => {
|
|
31
|
+
const value = credValues[key] ?? '';
|
|
32
|
+
if (value === '')
|
|
33
|
+
return;
|
|
34
|
+
setCredState((s) => ({ ...s, [key]: 'saving' }));
|
|
35
|
+
const ok = await setCredential(key, value);
|
|
36
|
+
setCredState((s) => ({ ...s, [key]: ok ? 'ok' : 'fail' }));
|
|
37
|
+
};
|
|
38
|
+
const copySource = async () => {
|
|
39
|
+
const text = (srcFile === '' || srcFile === 'SKILL.md') ? data?.content : srcContent;
|
|
40
|
+
if (text === undefined)
|
|
41
|
+
return;
|
|
42
|
+
try {
|
|
43
|
+
await navigator.clipboard.writeText(text);
|
|
44
|
+
setCopied(true);
|
|
45
|
+
setTimeout(() => setCopied(false), 1200);
|
|
46
|
+
}
|
|
47
|
+
catch { /* clipboard may be blocked */ }
|
|
48
|
+
};
|
|
49
|
+
const pickedScope = scopePick ?? scope?.declared ?? [];
|
|
50
|
+
/** Run one scope mutation, then report its outcome in place. */
|
|
51
|
+
const runScope = async (action) => {
|
|
52
|
+
setScopeBusy(true);
|
|
53
|
+
setScopeMsg(null);
|
|
54
|
+
try {
|
|
55
|
+
const result = await action();
|
|
56
|
+
setScopeMsg(result.ok ? t('scopeSaved') : `${t('scopeError')}: ${result.error ?? ''}`);
|
|
57
|
+
if (result.ok)
|
|
58
|
+
setScopePick(null);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
setScopeBusy(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const toggleScope = (id) => {
|
|
65
|
+
setScopePick(pickedScope.includes(id) ? pickedScope.filter(value => value !== id) : [...pickedScope, id]);
|
|
66
|
+
};
|
|
67
|
+
// Select a bundle file for the source pane. SKILL.md's body is already in the
|
|
68
|
+
// detail (no extra RPC); other files load on demand.
|
|
69
|
+
const selectSource = (path) => {
|
|
70
|
+
setSrcFile(path);
|
|
71
|
+
if (path === 'SKILL.md') {
|
|
72
|
+
setSrcLoading(false);
|
|
73
|
+
setSrcContent(data?.content);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
setSrcLoading(true);
|
|
77
|
+
setSrcContent(undefined);
|
|
78
|
+
void readSkillFile(name, path).then(res => {
|
|
79
|
+
setSrcContent(res?.content);
|
|
80
|
+
setSrcLoading(false);
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
return (_jsxs(ModalShell, { title: name, onClose: onClose, t: t, children: [claim.status === 'loading' ? _jsx("div", { className: css.empty, children: t('loading') }) : null, claim.status === 'done' && data === undefined ? _jsx("div", { className: css.empty, children: t('loadFailed') }) : null, claim.status === 'done' && data !== undefined ? (_jsxs(_Fragment, { children: [_jsx(MetadataRow, { items: [
|
|
84
|
+
{ label: t('source'), value: data.source },
|
|
85
|
+
{ label: t('provider'), value: data.provider },
|
|
86
|
+
{ label: t('modelInvocable'), value: data.modelInvocable ? t('yes') : t('no') },
|
|
87
|
+
...(data.whenToUse !== undefined ? [{ label: t('whenToUse'), value: data.whenToUse }] : []),
|
|
88
|
+
] }), _jsx("p", { className: css.detailDesc, children: data.description }), scope !== undefined ? (_jsxs("section", { className: css.scopeBox, children: [_jsxs("div", { className: css.scopeHead, children: [_jsx("span", { className: css.scopeTitle, children: t('scopeSection') }), _jsx("button", { type: "button", className: css.scopeInfo, title: `${t('scopeHint')}\n\n${t('scopeProbe')}`, "aria-label": t('scopeInfoLabel'), children: "i" })] }), scope.options.length === 0 ? (_jsx("div", { className: css.scopeHint, children: t('scopeUnavailable') })) : !scope.writable ? (_jsxs(_Fragment, { children: [_jsx("div", { className: css.scopeHint, children: providedScopeNote(t, scope.provider) }), modesLoading === true ? (_jsx("div", { className: css.scopeHint, children: t('modeBusy') })) : modeChips === undefined ? null : modeChips.length === 0 ? (_jsx("div", { className: css.scopeHint, children: t('modeNone') })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.scopeModes, children: [_jsx("span", { className: css.pvModesLabel, children: t('modeIn') }), modeChips.map(chip => (_jsx("button", { type: "button", className: css.modeChip, "data-default": chip.isDefault ? 'true' : undefined, title: t('modeChipHint'), onClick: () => onModeSelect?.(chip.id), children: chip.label }, chip.id)))] }), _jsx("div", { className: css.scopeHint, children: t('modeChipsCost') })] }))] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: css.scopeGrid, children: scope.options.map((option) => (_jsxs("label", { className: css.scopeItem, title: option.description, children: [_jsx("input", { type: "checkbox", checked: pickedScope.includes(option.id), disabled: scopeBusy, onChange: () => toggleScope(option.id) }), _jsx("span", { children: option.name ?? option.id }), option.broken !== undefined ? _jsx("span", { className: css.scopeBadge, children: t('scopeBroken') }) : null] }, option.id))) }), _jsxs("div", { className: css.scopeActions, children: [scope.managed ? (_jsx("button", { type: "button", className: css.addBtn, disabled: scopeBusy, onClick: () => void runScope(() => scope.save(pickedScope)), children: scopeBusy ? t('scopeSaving') : t('scopeSave') })) : null, scope.managed ? (_jsx("button", { type: "button", className: css.addBtn, disabled: scopeBusy, onClick: () => void runScope(() => scope.release()), children: t('scopeRelease') })) : scope.adoptable ? (_jsx("button", { type: "button", className: css.addBtn, disabled: scopeBusy, onClick: () => void runScope(() => scope.adopt(pickedScope)), children: t('scopeAdopt') })) : null, scopeMsg !== null ? _jsx("span", { className: css.scopeHint, children: scopeMsg }) : null] }), scope.conflict !== undefined ? (_jsxs("div", { className: css.scopeConflict, children: [t('scopeConflict'), ": ", scope.conflict] })) : null] }))] })) : null, data.metadataText !== undefined && hasUnrenderedMetadata(data.metadataText) ? (_jsxs("details", { className: css.source, children: [_jsx("summary", { className: css.sourceTitle, children: t('frontmatterMetadata') }), _jsx("pre", { className: css.codeBlk, children: formatMetadata(data.metadataText) })] })) : null, data.credentials !== undefined && data.credentials.length > 0 ? (_jsxs("details", { className: css.conf, open: true, children: [_jsx("summary", { className: css.confTitle, children: t('credentials') }), _jsx("div", { className: css.confHint, children: t('credentialsHint') }), data.credentials.map((decl) => {
|
|
89
|
+
const key = decl.key;
|
|
90
|
+
const label = decl.label ?? key;
|
|
91
|
+
const state = credState[key] ?? 'idle';
|
|
92
|
+
const configured = decl.configured || state === 'ok';
|
|
93
|
+
return (_jsx(CredentialField, { label: label, configured: configured, value: credValues[key] ?? '', state: state, onChange: (value) => setCredValues((s) => ({ ...s, [key]: value })), onSave: () => void saveCred(key), t: t }, key));
|
|
94
|
+
})] })) : null, data.environmentRefs !== undefined && data.environmentRefs.length > 0 ? (_jsxs("details", { className: css.conf, children: [_jsx("summary", { className: css.confTitle, children: t('envRefs') }), _jsx("div", { className: css.confHint, children: t('envRefsHint') }), _jsx("div", { className: css.envRefList, children: data.environmentRefs.map((ref) => (_jsx("span", { className: css.envRef, children: ref }, ref))) })] })) : null, (() => {
|
|
95
|
+
const files = data.files !== undefined && data.files.length > 0 ? data.files : ['SKILL.md'];
|
|
96
|
+
const single = files.length <= 1;
|
|
97
|
+
return (_jsxs("section", { className: css.sourceSection, children: [_jsxs("button", { type: "button", className: css.sourceTrigger, onClick: () => setSourceOpen((o) => !o), "aria-expanded": sourceOpen, children: [_jsx("span", { className: css.sourceChevron, children: sourceOpen ? _jsx(IconChevronDownOutlineMedium, { size: 16 }) : _jsx(IconChevronRightOutlineMedium, { size: 16 }) }), _jsx("span", { className: css.sourceLabel, children: t('viewSource') })] }), sourceOpen ? (single ? (_jsxs("div", { className: css.sourceSingle, children: [_jsxs("div", { className: css.sourceBar, children: [_jsx("span", { className: css.sourceBarFile, children: srcFile === '' || srcFile === 'SKILL.md' ? 'SKILL.md' : srcFile }), _jsx("button", { type: "button", className: css.sourceCopy, onClick: () => void copySource(), "aria-label": t('copy'), children: copied ? t('copied') : _jsxs("span", { className: css.codeCopyIcon, children: [_jsx(IconCopyOutlineMedium, { size: 16 }), " ", t('copy')] }) })] }), _jsx("div", { className: css.detailPane, children: srcLoading ? _jsx("div", { className: css.empty, children: t('loading') })
|
|
98
|
+
: (srcFile === '' || srcFile === 'SKILL.md') ? _jsx("pre", { className: css.codeBlk, children: data.content })
|
|
99
|
+
: srcContent === undefined ? _jsx("div", { className: css.empty, children: t('loadFailed') })
|
|
100
|
+
: _jsx("pre", { className: css.codeBlk, children: srcContent }) })] })) : (_jsxs("div", { className: css.split, children: [_jsx("div", { className: css.treePane, children: _jsx(BundleFileTree, { files: files, selectedPath: srcFile === '' ? 'SKILL.md' : srcFile, onSelect: selectSource }) }), _jsxs("div", { className: css.detailPane, children: [_jsx("button", { type: "button", className: css.codeCopy, onClick: () => void copySource(), "aria-label": t('copy'), children: copied ? _jsx("span", { children: t('copied') }) : _jsxs("span", { className: css.codeCopyIcon, children: [_jsx(IconCopyOutlineMedium, { size: 16 }), " ", t('copy')] }) }), srcLoading ? _jsx("div", { className: css.empty, children: t('loading') })
|
|
101
|
+
: (srcFile === '' || srcFile === 'SKILL.md') ? _jsx("pre", { className: css.codeBlk, children: data.content })
|
|
102
|
+
: srcContent === undefined ? _jsx("div", { className: css.empty, children: t('loadFailed') })
|
|
103
|
+
: _jsx("pre", { className: css.codeBlk, children: srcContent })] })] }))) : null] }));
|
|
104
|
+
})()] })) : null] }));
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Metadata keys this modal already renders in a dedicated place.
|
|
108
|
+
*
|
|
109
|
+
* `presetScope` is edited by the scope section and visible in the source pane;
|
|
110
|
+
* `credentials` is the credential form above (its values live in the credential
|
|
111
|
+
* store, never in the frontmatter). Repeating either as raw JSON is noise, and
|
|
112
|
+
* an empty `metadata` block appearing because someone declared a credential is
|
|
113
|
+
* worse than noise — it reads as a defect.
|
|
114
|
+
*/
|
|
115
|
+
const RENDERED_METADATA_KEYS = new Set(['presetScope', 'credentials']);
|
|
116
|
+
/**
|
|
117
|
+
* Whether a skill's metadata holds anything the modal does not already render.
|
|
118
|
+
* @param metadataText - the serialized metadata object.
|
|
119
|
+
* @returns whether the block is worth showing.
|
|
120
|
+
*/
|
|
121
|
+
export function hasUnrenderedMetadata(metadataText) {
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(metadataText);
|
|
124
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
125
|
+
return true;
|
|
126
|
+
return Object.keys(parsed).some(key => !RENDERED_METADATA_KEYS.has(key));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Keys whose VALUE is replaced before a metadata dump is displayed. */
|
|
133
|
+
const SECRET_KEY_PATTERN = /key|token|secret|password/i;
|
|
134
|
+
/** The placeholder a secret-shaped metadata value renders as. */
|
|
135
|
+
const REDACTED = '···';
|
|
136
|
+
/**
|
|
137
|
+
* Replace secret-shaped VALUES in a metadata object before display.
|
|
138
|
+
*
|
|
139
|
+
* The metadata block prints whatever frontmatter carries, and today nothing
|
|
140
|
+
* secret lives there (credential values are stored in the credential service,
|
|
141
|
+
* never in the file). This is a display guard for the day someone puts an
|
|
142
|
+
* inline token in a metadata key: the block still shows the key and its shape,
|
|
143
|
+
* but not the value. Matching is on the KEY (recursively, so a nested
|
|
144
|
+
* `auth.token` is covered) — a value-shaped heuristic would redact prose.
|
|
145
|
+
* @param value - any parsed metadata value.
|
|
146
|
+
* @returns the same shape with secret-keyed values replaced.
|
|
147
|
+
*/
|
|
148
|
+
export function redactMetadataSecrets(value) {
|
|
149
|
+
if (Array.isArray(value))
|
|
150
|
+
return value.map(entry => redactMetadataSecrets(entry));
|
|
151
|
+
if (value === null || typeof value !== 'object')
|
|
152
|
+
return value;
|
|
153
|
+
const out = {};
|
|
154
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
155
|
+
out[key] = SECRET_KEY_PATTERN.test(key) ? REDACTED : redactMetadataSecrets(entry);
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
/** Pretty-print a JSON metadata string for display, with secret-shaped values redacted. */
|
|
160
|
+
export function formatMetadata(metadataText) {
|
|
161
|
+
try {
|
|
162
|
+
return JSON.stringify(redactMetadataSecrets(JSON.parse(metadataText)), null, 2);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return metadataText;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CatalogToolRow } from '@khorsheed/dsh-capability-catalog/types';
|
|
2
|
+
import type { CapabilityCatalogKey } from './locales.ts';
|
|
3
|
+
import type { CatalogModeChip, ModeComparison } from './mode-model.ts';
|
|
4
|
+
import type { McpGroup } from './mcp-model.ts';
|
|
5
|
+
export type ToolSegment = 'all' | 'builtin' | 'plugin' | 'mcp';
|
|
6
|
+
/** Human label for one tool's channel pill. */
|
|
7
|
+
export declare function toolTag(tool: CatalogToolRow, t: (key: CapabilityCatalogKey) => string): string;
|
|
8
|
+
/** Origin / ownership subtitle for a tool card. */
|
|
9
|
+
export declare function toolOrigin(tool: CatalogToolRow, t: (key: CapabilityCatalogKey) => string): string;
|
|
10
|
+
/** The tools tab's card views following the segment filter. `all` renders both the
|
|
11
|
+
* builtin/plugin tool cards AND the MCP server cards in ONE grid (so the 16px gap
|
|
12
|
+
* stays uniform across them); the other segments render one set. */
|
|
13
|
+
export declare function ToolCards({ loading, visibleTools, mcpGroups, segment, comparison, modeChipsFor, onMode, onOpenTool, onOpenServer, onSetEnabled, onRemove, t }: {
|
|
14
|
+
loading: boolean;
|
|
15
|
+
visibleTools: readonly CatalogToolRow[];
|
|
16
|
+
mcpGroups: readonly McpGroup[];
|
|
17
|
+
segment: ToolSegment;
|
|
18
|
+
/** The comparison model, or null when the grid shows one mode's face. */
|
|
19
|
+
comparison: ModeComparison | null;
|
|
20
|
+
modeChipsFor: (ids: readonly string[] | undefined) => readonly CatalogModeChip[];
|
|
21
|
+
onMode: (id: string) => void;
|
|
22
|
+
onOpenTool: (tool: CatalogToolRow) => void;
|
|
23
|
+
onOpenServer: (name: string) => void;
|
|
24
|
+
onSetEnabled: (serverName: string, enabled: boolean) => Promise<void>;
|
|
25
|
+
onRemove: (serverName: string) => Promise<void>;
|
|
26
|
+
t: (key: CapabilityCatalogKey) => string;
|
|
27
|
+
}): import("react").JSX.Element | null;
|
|
28
|
+
//# sourceMappingURL=ToolCards.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { IconBrowseOutlineMedium } from './icons.js';
|
|
3
|
+
import { McpCard } from './McpCards.js';
|
|
4
|
+
import { ModeChips } from './ModeChips.js';
|
|
5
|
+
import css from './CapabilityCatalogCard.module.css';
|
|
6
|
+
/** Human label for one tool's channel pill. */
|
|
7
|
+
export function toolTag(tool, t) {
|
|
8
|
+
if (tool.channel === 'mcp')
|
|
9
|
+
return tool.serverName !== undefined ? `MCP · ${tool.serverName}` : 'MCP';
|
|
10
|
+
if (tool.channel === 'plugin')
|
|
11
|
+
return t('toolPlugin');
|
|
12
|
+
if (tool.channel === 'builtin')
|
|
13
|
+
return t('toolBuiltin');
|
|
14
|
+
return tool.channel;
|
|
15
|
+
}
|
|
16
|
+
/** Origin / ownership subtitle for a tool card. */
|
|
17
|
+
export function toolOrigin(tool, t) {
|
|
18
|
+
if (tool.channel === 'mcp')
|
|
19
|
+
return tool.serverName ?? 'MCP';
|
|
20
|
+
if (tool.channel === 'plugin')
|
|
21
|
+
return tool.owner ?? t('toolPlugin');
|
|
22
|
+
return t('toolBuiltin');
|
|
23
|
+
}
|
|
24
|
+
/** Tool preview card — same anatomy as the skill cards (shared .pvCard / .grid),
|
|
25
|
+
* so tools and skills carry ONE style that can be optimized together. In the
|
|
26
|
+
* comparison view a chip row names the modes that load it. */
|
|
27
|
+
function ToolCard({ tool, modes, modeTotal, onMode, onOpen, t }) {
|
|
28
|
+
return (_jsxs("div", { className: css.pvCard, "data-modes": modes === undefined ? undefined : 'true', children: [_jsxs("button", { type: "button", className: css.pvMain, onClick: onOpen, children: [_jsxs("span", { className: css.pvHead, children: [_jsx("span", { className: css.pvName, children: tool.name }), _jsx("span", { className: `${css.pvTag} ${tool.channel === 'mcp' ? css.tagMcp : tool.channel === 'plugin' ? css.tagPlugin : ''}`, children: toolTag(tool, t) })] }), _jsx("span", { className: css.pvDesc, children: tool.description }), _jsx("span", { className: css.pvSub, children: toolOrigin(tool, t) })] }), modes === undefined ? null : _jsx(ModeChips, { modes: modes, total: modeTotal ?? 0, onSelect: onMode, t: t }), _jsx("div", { className: css.pvFoot, children: _jsx("button", { type: "button", className: css.iconButton, onClick: onOpen, "aria-label": t('viewDetail'), title: t('viewDetail'), children: _jsx(IconBrowseOutlineMedium, { size: 16 }) }) })] }));
|
|
29
|
+
}
|
|
30
|
+
/** The tools tab's card views following the segment filter. `all` renders both the
|
|
31
|
+
* builtin/plugin tool cards AND the MCP server cards in ONE grid (so the 16px gap
|
|
32
|
+
* stays uniform across them); the other segments render one set. */
|
|
33
|
+
export function ToolCards({ loading, visibleTools, mcpGroups, segment, comparison, modeChipsFor, onMode, onOpenTool, onOpenServer, onSetEnabled, onRemove, t }) {
|
|
34
|
+
if (loading)
|
|
35
|
+
return null;
|
|
36
|
+
const showsToolCards = segment !== 'mcp';
|
|
37
|
+
const showsMcpServers = segment !== 'builtin' && segment !== 'plugin';
|
|
38
|
+
const total = (showsToolCards ? visibleTools.length : 0) + (showsMcpServers ? mcpGroups.length : 0);
|
|
39
|
+
/** The chips for one tool, or undefined when it has no row (see the skill card). */
|
|
40
|
+
const chipsForTool = (tool) => {
|
|
41
|
+
if (comparison === null)
|
|
42
|
+
return undefined;
|
|
43
|
+
const chips = modeChipsFor(comparison.toolModes.get(tool.name));
|
|
44
|
+
return chips.length === 0 ? undefined : chips;
|
|
45
|
+
};
|
|
46
|
+
if (total === 0) {
|
|
47
|
+
return segment === 'mcp'
|
|
48
|
+
? _jsx("div", { className: css.empty, children: t('mcpServerEmpty') })
|
|
49
|
+
: _jsx("div", { className: css.empty, children: t('toolNoMatch') });
|
|
50
|
+
}
|
|
51
|
+
return (_jsxs("div", { className: css.grid, children: [showsToolCards ? visibleTools.map((tool) => (_jsx(ToolCard, { tool: tool, modes: chipsForTool(tool), modeTotal: comparison?.modes ?? 0, onMode: onMode, onOpen: () => onOpenTool(tool), t: t }, tool.name))) : null, showsMcpServers ? mcpGroups.map((g) => (_jsx(McpCard, { group: g, onOpen: () => onOpenServer(g.serverName), onSetEnabled: onSetEnabled, onRemove: onRemove, t: t }, g.serverName))) : null] }));
|
|
52
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CatalogToolRow } from '@khorsheed/dsh-capability-catalog/types';
|
|
2
|
+
import type { CapabilityCatalogKey } from './locales.ts';
|
|
3
|
+
/** Tool detail modal: description (clamped when it overflows, expandable) +
|
|
4
|
+
* channel/origin + parameter schema tree. Also serves MCP tools opened from the
|
|
5
|
+
* server manage modal — one detail view for every tool. No footer button — the
|
|
6
|
+
* ×, Esc, and mask-click close it (ModalShell). */
|
|
7
|
+
export declare function ToolDetailModal({ tool, onClose, t }: {
|
|
8
|
+
tool: CatalogToolRow;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
t: (key: CapabilityCatalogKey) => string;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=ToolDetailModal.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { ModalShell } from './ModalShell.js';
|
|
4
|
+
import { MetadataRow } from './MetadataRow.js';
|
|
5
|
+
import { SchemaView } from './SchemaView.js';
|
|
6
|
+
import { toolOrigin, toolTag } from './ToolCards.js';
|
|
7
|
+
import css from './CapabilityCatalogCard.module.css';
|
|
8
|
+
/** Tool detail modal: description (clamped when it overflows, expandable) +
|
|
9
|
+
* channel/origin + parameter schema tree. Also serves MCP tools opened from the
|
|
10
|
+
* server manage modal — one detail view for every tool. No footer button — the
|
|
11
|
+
* ×, Esc, and mask-click close it (ModalShell). */
|
|
12
|
+
export function ToolDetailModal({ tool, onClose, t }) {
|
|
13
|
+
const [descExpanded, setDescExpanded] = useState(false);
|
|
14
|
+
// Show the expand toggle only when the description actually overflows the clamp.
|
|
15
|
+
const descRef = useRef(null);
|
|
16
|
+
const [descOverflow, setDescOverflow] = useState(false);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const el = descRef.current;
|
|
19
|
+
if (el === null) {
|
|
20
|
+
setDescOverflow(false);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
setDescOverflow(el.scrollHeight > el.clientHeight + 1);
|
|
24
|
+
}, [tool.description]);
|
|
25
|
+
return (_jsxs(ModalShell, { title: tool.name, onClose: onClose, t: t, children: [_jsx(MetadataRow, { items: [
|
|
26
|
+
{ label: t('source'), value: toolTag(tool, t) },
|
|
27
|
+
{ label: t('provider'), value: toolOrigin(tool, t) },
|
|
28
|
+
] }), _jsxs("div", { className: css.toolDetailDesc, children: [_jsx("p", { ref: descRef, className: `${css.toolDesc} ${descExpanded ? css.toolDescExpanded : css.toolDescClamp}`, children: tool.description }), descOverflow ? (_jsx("button", { type: "button", className: css.toolDescToggle, onClick: () => setDescExpanded(e => !e), children: descExpanded ? t('toolCollapse') : t('toolExpand') })) : null] }), _jsx(SchemaView, { parameters: tool.parameters, title: t('toolParams'), t: t })] }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated by scripts/sync-icon-artwork.mts from @deepseek-ai/dsh-client-ui-primitives
|
|
3
|
+
* (v0.1.7-rc.1, harness 46a7f68b09) — do not edit by hand. The 0.1.5 and rc.1 host lines
|
|
4
|
+
* share no icon export name, so the rc.1 artwork is inlined here: pure SVG,
|
|
5
|
+
* no host runtime identity, identical on every line. Re-run the generator
|
|
6
|
+
* after adding an icon import or bumping the harness checkout.
|
|
7
|
+
*
|
|
8
|
+
* @module @khorsheed/dsh-capability-catalog/client
|
|
9
|
+
*/
|
|
10
|
+
/** Shared props for every product icon component (mirrors the upstream IconProps). */
|
|
11
|
+
export interface IconProps {
|
|
12
|
+
/** Square edge in px; defaults to the glyph's own drawn size. */
|
|
13
|
+
size?: number | undefined;
|
|
14
|
+
/** Extra class for layout placement; color rides currentColor. */
|
|
15
|
+
className?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
export declare const IconBrowseOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
|
|
18
|
+
export declare const IconChevronDownOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
|
|
19
|
+
export declare const IconChevronRightOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
|
|
20
|
+
export declare const IconCopyOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
|
|
21
|
+
export declare const IconFolderCloseMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
|
|
22
|
+
export declare const IconFolderOpenMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
|
|
23
|
+
export declare const IconFolderOpenOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
|
|
24
|
+
export declare const IconSearchOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
|
|
25
|
+
export declare const IconTrashOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
|
|
26
|
+
//# sourceMappingURL=icons.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export const IconBrowseOutlineMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M4.9375 5.90295H11.0625", stroke: "currentColor" }), _jsx("path", { d: "M4.9375 9.02991H8.27841", stroke: "currentColor" }), _jsx("path", { d: "M12.5 1.32617C13.3039 1.32617 14 1.95171 14 2.77637V13.2246C13.9996 14.0489 13.3036 14.6738 12.5 14.6738H3.5C2.69637 14.6738 2.00042 14.0489 2 13.2246V2.77637C2 1.95171 2.69613 1.32617 3.5 1.32617H12.5ZM3.5 2.32617C3.1993 2.32617 3 2.55186 3 2.77637V13.2246C3.00044 13.4489 3.19963 13.6738 3.5 13.6738H12.5C12.8004 13.6738 12.9996 13.4489 13 13.2246V2.77637C13 2.55186 12.8007 2.32617 12.5 2.32617H3.5Z", fill: "currentColor" })] }));
|
|
3
|
+
export const IconChevronDownOutlineMedium = ({ size = 14, className }) => (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: _jsx("path", { d: "M4 6L7.29289 9.29289C7.68342 9.68342 8.31658 9.68342 8.70711 9.29289L12 6", stroke: "currentColor" }) }));
|
|
4
|
+
export const IconChevronRightOutlineMedium = ({ size = 14, className }) => (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: _jsx("path", { d: "M6 12L9.29289 8.70711C9.68342 8.31658 9.68342 7.68342 9.29289 7.29289L6 4", stroke: "currentColor" }) }));
|
|
5
|
+
export const IconCopyOutlineMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("rect", { x: "1.52075", y: "4.07373", width: "10.3932", height: "10.3932", rx: "2", stroke: "currentColor" }), _jsx("path", { d: "M11.9792 1.53296C13.36 1.53296 14.4792 2.65225 14.4792 4.03296V9.42847C14.4792 10.3756 13.9521 11.1987 13.1755 11.6228V10.3298C13.3652 10.0787 13.4792 9.7674 13.4792 9.42847V4.03296C13.4792 3.20453 12.8077 2.53296 11.9792 2.53296H6.58374C6.27966 2.53301 5.99684 2.6235 5.7605 2.77905H4.42358C4.85652 2.03463 5.66056 1.53304 6.58374 1.53296H11.9792Z", fill: "currentColor" })] }));
|
|
6
|
+
export const IconFolderCloseMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M1.50439 3.11059C1.50439 2.55831 1.95211 2.1106 2.50439 2.1106H5.43389C5.67773 2.1106 5.91318 2.19969 6.09593 2.36113L7.71649 3.79265C7.89924 3.95409 8.1347 4.04319 8.3785 4.04319H13.4958C14.0481 4.04319 14.4958 4.4909 14.4958 5.04319V12.8894C14.4958 13.4417 14.0481 13.8894 13.4958 13.8894H2.50439C1.95211 13.8894 1.50439 13.4417 1.50439 12.8894V4.04319V3.11059Z", stroke: "currentColor" }), _jsx("path", { d: "M3.63501 7.66614H12.3647", stroke: "currentColor" })] }));
|
|
7
|
+
export const IconFolderOpenMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M2.55912 7.93683C2.67584 7.49906 3.0723 7.19446 3.52536 7.19446H13.6491C14.3061 7.19446 14.7846 7.81725 14.6153 8.45209L13.4411 12.856C13.3244 13.2938 12.9279 13.5984 12.4748 13.5984H2.35113C1.69411 13.5984 1.21562 12.9756 1.38489 12.3407L2.55912 7.93683Z", fill: "currentColor", opacity: "0.16" }), _jsx("path", { d: "M13.6491 6.69446C14.6346 6.69453 15.3522 7.62895 15.0983 8.58118L13.9245 12.9845C13.7494 13.6412 13.1539 14.0988 12.4743 14.0988H2.35126C1.36574 14.0988 0.648153 13.1643 0.902044 12.212L2.07587 7.80774C2.25102 7.15128 2.84567 6.69455 3.52509 6.69446H13.6491ZM3.52509 7.69446C3.29865 7.69455 3.10004 7.84674 3.04169 8.06555L1.86786 12.4698C1.78345 12.7872 2.02285 13.0988 2.35126 13.0988H12.4743C12.7007 13.0988 12.8992 12.9463 12.9577 12.7277L14.1325 8.32336C14.2171 8.00598 13.9776 7.69453 13.6491 7.69446H3.52509Z", fill: "currentColor" }), _jsx("path", { d: "M4.7666 1.90137C5.13227 1.90144 5.48571 2.03525 5.75977 2.27734L7.27246 3.61328C7.36379 3.69382 7.48174 3.73828 7.60352 3.73828H12.3994C13.2276 3.73841 13.8993 4.41005 13.8994 5.23828V6.7168C13.8183 6.70327 13.735 6.69436 13.6494 6.69434H12.8994V5.23828C12.8993 4.96233 12.6754 4.73841 12.3994 4.73828H7.60352C7.23781 4.73828 6.88446 4.60438 6.61035 4.3623L5.09766 3.02637C5.00636 2.94576 4.88838 2.90144 4.7666 2.90137H2.0498C1.77366 2.90137 1.5498 3.12523 1.5498 3.40137V9.78223L0.902344 12.2119C0.648452 13.1642 1.36604 14.0986 2.35156 14.0986H2.0498C1.2214 14.0986 0.549838 13.427 0.549805 12.5986V3.40137C0.549805 2.57294 1.22138 1.90137 2.0498 1.90137H4.7666Z", fill: "currentColor" })] }));
|
|
8
|
+
export const IconFolderOpenOutlineMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M12.3994 13.5986H2.04956C1.49728 13.5986 1.04956 13.1509 1.04956 12.5986V3.40137C1.04956 2.84908 1.49728 2.40137 2.04956 2.40137H4.76632C5.01016 2.40137 5.24561 2.49046 5.42836 2.6519L6.94088 3.98799C7.12364 4.14943 7.35908 4.23852 7.60293 4.23852H12.3994C12.9517 4.23852 13.3994 4.68624 13.3994 5.23852V7.16991", stroke: "currentColor" }), _jsx("path", { d: "M2.55911 7.93683C2.67584 7.49906 3.07229 7.19446 3.52536 7.19446H13.6491C14.3061 7.19446 14.7846 7.81725 14.6153 8.45209L13.4411 12.856C13.3244 13.2938 12.9279 13.5984 12.4748 13.5984H2.35113C1.69411 13.5984 1.21562 12.9756 1.38489 12.3407L2.55911 7.93683Z", stroke: "currentColor" })] }));
|
|
9
|
+
export const IconSearchOutlineMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M6.58727 11.8586C9.55061 11.8586 11.9529 9.45637 11.9529 6.49304C11.9529 3.5297 9.55061 1.12744 6.58727 1.12744C3.62394 1.12744 1.22168 3.5297 1.22168 6.49304C1.22168 9.45637 3.62394 11.8586 6.58727 11.8586Z", stroke: "currentColor" }), _jsx("path", { d: "M10.2991 10.3933L14.7783 14.8725", stroke: "currentColor" })] }));
|
|
10
|
+
export const IconTrashOutlineMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M1.28149 3.88831H14.7187", stroke: "currentColor" }), _jsx("path", { d: "M5.41602 3.88833V2.47962C5.41602 2.29282 5.52492 2.11366 5.71876 1.98157C5.9126 1.84948 6.17551 1.77527 6.44964 1.77527H9.55053C9.82466 1.77527 10.0876 1.84948 10.2814 1.98157C10.4753 2.11366 10.5842 2.29282 10.5842 2.47962V3.88833", stroke: "currentColor" }), _jsx("path", { d: "M2.57349 3.88831L3.19366 13.2943C3.21937 13.5502 3.33952 13.7872 3.53065 13.9593C3.72178 14.1313 3.97016 14.2259 4.22729 14.2246H11.7728C12.0299 14.2259 12.2783 14.1313 12.4694 13.9593C12.6605 13.7872 12.7807 13.5502 12.8064 13.2943L13.4266 3.88831", stroke: "currentColor" }), _jsx("path", { d: "M6.44946 6.98926V11.1238", stroke: "currentColor" }), _jsx("path", { d: "M9.55054 6.98926V11.1238", stroke: "currentColor" })] }));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability-catalog client plugin, browser half. Mounts the generated
|
|
3
|
+
* `capabilityCatalog` Remote namespace and registers a standalone settings
|
|
4
|
+
* section (`settings.section`) named 工具与技能, which lists the registered
|
|
5
|
+
* skills as collapsible cards.
|
|
6
|
+
* @module @khorsheed/dsh-capability-catalog/client
|
|
7
|
+
*/
|
|
8
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
9
|
+
import type { TypertRemoteNamespaceMap } from '@deepseek-ai/dsh-typert-protocol';
|
|
10
|
+
/** The capabilityCatalog Remote namespace, as mounted by this plugin. */
|
|
11
|
+
export type CapabilityCatalogRemote = TypertRemoteNamespaceMap['capabilityCatalog'];
|
|
12
|
+
/** Required services: the slot ledger, the remote channel, and the copy. */
|
|
13
|
+
export declare const inject: string[];
|
|
14
|
+
/**
|
|
15
|
+
* Client plugin body: mount the Remote, register the dictionaries, and the
|
|
16
|
+
* settings card.
|
|
17
|
+
* @param ctx - client root context.
|
|
18
|
+
* @returns disposer unwinding the mounted Remote namespace.
|
|
19
|
+
*/
|
|
20
|
+
export declare function apply(ctx: Context): Promise<() => Promise<void>>;
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import catalogRemote from '@khorsheed/dsh-capability-catalog/remote';
|
|
2
|
+
import { en, NS, zh } from './locales.js';
|
|
3
|
+
import { refreshUntilSettled } from './settle.js';
|
|
4
|
+
import { CapabilityCatalogCard } from './CapabilityCatalogCard.js';
|
|
5
|
+
/** Required services: the slot ledger, the remote channel, and the copy. */
|
|
6
|
+
export const inject = ['slots', 'remote', 'locale'];
|
|
7
|
+
/**
|
|
8
|
+
* Client plugin body: mount the Remote, register the dictionaries, and the
|
|
9
|
+
* settings card.
|
|
10
|
+
* @param ctx - client root context.
|
|
11
|
+
* @returns disposer unwinding the mounted Remote namespace.
|
|
12
|
+
*/
|
|
13
|
+
export async function apply(ctx) {
|
|
14
|
+
const disposers = [];
|
|
15
|
+
try {
|
|
16
|
+
disposers.push(await ctx.remote.$mount(catalogRemote));
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
ctx.logger.error(error);
|
|
20
|
+
}
|
|
21
|
+
const remote = ctx.get('remote.capabilityCatalog');
|
|
22
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'capability-catalog: dictionaries');
|
|
23
|
+
const t = ctx.locale.bind(NS);
|
|
24
|
+
// A tiny external store holding the last snapshot; refreshed on open and on
|
|
25
|
+
// every mode switch. `presetId` names the MODE the grid is showing: it rides
|
|
26
|
+
// into the scope of the read, so the panel's data and its mode control can
|
|
27
|
+
// never disagree about which face is on screen.
|
|
28
|
+
let snapshot;
|
|
29
|
+
const listeners = new Set();
|
|
30
|
+
const refresh = async (presetId) => {
|
|
31
|
+
try {
|
|
32
|
+
const carried = await remote?.snapshotAt(presetId, undefined);
|
|
33
|
+
if (carried === undefined || !carried.ok)
|
|
34
|
+
return;
|
|
35
|
+
snapshot = carried.value;
|
|
36
|
+
for (const l of [...listeners])
|
|
37
|
+
l();
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
ctx.logger.error(error);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const catalogHook = {
|
|
44
|
+
getSnapshot: () => snapshot,
|
|
45
|
+
subscribe: (listener) => {
|
|
46
|
+
listeners.add(listener);
|
|
47
|
+
return () => listeners.delete(listener);
|
|
48
|
+
},
|
|
49
|
+
refresh,
|
|
50
|
+
};
|
|
51
|
+
// Every mode's face, read ONCE per session and shared by both consumers: the
|
|
52
|
+
// comparison grid and a detail modal asking where a plugin-provided skill is
|
|
53
|
+
// loadable. Reading it composes presets nothing has mounted, so a second
|
|
54
|
+
// caller must never pay for it again — hence the cache plus a single-flight
|
|
55
|
+
// promise (two opens racing would otherwise compose the same presets twice).
|
|
56
|
+
// `force` re-reads after a mutation that can change a face (an MCP connect, a
|
|
57
|
+
// skill write), and clears both slots so a failed read is retried too.
|
|
58
|
+
let facesCache;
|
|
59
|
+
let facesPending;
|
|
60
|
+
const modeFaces = async (force = false) => {
|
|
61
|
+
if (force) {
|
|
62
|
+
facesCache = undefined;
|
|
63
|
+
facesPending = undefined;
|
|
64
|
+
}
|
|
65
|
+
if (facesCache !== undefined)
|
|
66
|
+
return facesCache;
|
|
67
|
+
facesPending ??= (async () => {
|
|
68
|
+
try {
|
|
69
|
+
const carried = await remote?.modeFaces(undefined);
|
|
70
|
+
return carried !== undefined && carried.ok ? carried.value : [];
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
ctx.logger.error(error);
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
})();
|
|
77
|
+
const faces = await facesPending;
|
|
78
|
+
facesCache = faces;
|
|
79
|
+
return faces;
|
|
80
|
+
};
|
|
81
|
+
/** Drop the cached faces after a write that can change what a mode holds. */
|
|
82
|
+
const invalidateModeFaces = () => {
|
|
83
|
+
facesCache = undefined;
|
|
84
|
+
facesPending = undefined;
|
|
85
|
+
};
|
|
86
|
+
const injected = {
|
|
87
|
+
hooks: {
|
|
88
|
+
catalog: catalogHook,
|
|
89
|
+
},
|
|
90
|
+
refresh,
|
|
91
|
+
// A skill write reaches the registry through the host's watcher, a moment
|
|
92
|
+
// after the operation returns; one refresh therefore reads stale rows.
|
|
93
|
+
refreshSettled: (settled, presetId) => refreshUntilSettled(() => snapshot, () => refresh(presetId), settled),
|
|
94
|
+
detail: async (name, presetId) => {
|
|
95
|
+
const carried = await remote?.detail(name, undefined, presetId);
|
|
96
|
+
return carried !== undefined && carried.ok ? carried.value : undefined;
|
|
97
|
+
},
|
|
98
|
+
readSkillFile: async (name, path, presetId) => {
|
|
99
|
+
const carried = await remote?.readSkillFile(name, path, undefined, presetId);
|
|
100
|
+
return carried !== undefined && carried.ok ? carried.value : undefined;
|
|
101
|
+
},
|
|
102
|
+
listDirSkills: async (dirPath) => {
|
|
103
|
+
const carried = await remote?.listDirSkills(dirPath);
|
|
104
|
+
return carried !== undefined && carried.ok ? carried.value : [];
|
|
105
|
+
},
|
|
106
|
+
setCredential: async (key, value) => {
|
|
107
|
+
const carried = await remote?.setCredential({ key, value });
|
|
108
|
+
return carried !== undefined && carried.ok ? carried.value : false;
|
|
109
|
+
},
|
|
110
|
+
addSkill: async (request) => {
|
|
111
|
+
const carried = await remote?.addSkill(request);
|
|
112
|
+
invalidateModeFaces();
|
|
113
|
+
return carried !== undefined && carried.ok ? carried.value : { ok: false, error: 'remote absent' };
|
|
114
|
+
},
|
|
115
|
+
deleteSkill: async (name, presetId) => {
|
|
116
|
+
const carried = await remote?.deleteSkill(name, undefined, presetId);
|
|
117
|
+
invalidateModeFaces();
|
|
118
|
+
return carried !== undefined && carried.ok ? carried.value : { ok: false, error: 'remote absent' };
|
|
119
|
+
},
|
|
120
|
+
pickDirectory: async () => {
|
|
121
|
+
const carried = await remote?.pickDirectory();
|
|
122
|
+
return carried !== undefined && carried.ok ? carried.value : null;
|
|
123
|
+
},
|
|
124
|
+
modeFaces,
|
|
125
|
+
mcpSnapshot: async () => {
|
|
126
|
+
const carried = await remote?.mcpSnapshot();
|
|
127
|
+
return carried !== undefined && carried.ok ? carried.value : { servers: [], tools: {}, credentials: [] };
|
|
128
|
+
},
|
|
129
|
+
mcpAdd: async (config) => {
|
|
130
|
+
const carried = await remote?.mcpAdd(config);
|
|
131
|
+
invalidateModeFaces();
|
|
132
|
+
return carried !== undefined && carried.ok ? carried.value : false;
|
|
133
|
+
},
|
|
134
|
+
mcpRemove: async (serverName) => {
|
|
135
|
+
const carried = await remote?.mcpRemove(serverName);
|
|
136
|
+
invalidateModeFaces();
|
|
137
|
+
return carried !== undefined && carried.ok ? carried.value : false;
|
|
138
|
+
},
|
|
139
|
+
mcpSetEnabled: async (serverName, enabled) => {
|
|
140
|
+
const carried = await remote?.mcpSetEnabled(serverName, enabled);
|
|
141
|
+
invalidateModeFaces();
|
|
142
|
+
return carried !== undefined && carried.ok ? carried.value : undefined;
|
|
143
|
+
},
|
|
144
|
+
mcpSetCredential: async (ref, value) => {
|
|
145
|
+
const carried = await remote?.mcpSetCredential(ref, value);
|
|
146
|
+
return carried !== undefined && carried.ok ? carried.value : false;
|
|
147
|
+
},
|
|
148
|
+
mcpSetToolEnabled: async (serverName, tool, enabled) => {
|
|
149
|
+
const carried = await remote?.mcpSetToolEnabled(serverName, tool, enabled);
|
|
150
|
+
invalidateModeFaces();
|
|
151
|
+
return carried !== undefined && carried.ok ? carried.value : undefined;
|
|
152
|
+
},
|
|
153
|
+
mcpDiscover: async (serverName) => {
|
|
154
|
+
const carried = await remote?.mcpDiscover(serverName);
|
|
155
|
+
invalidateModeFaces();
|
|
156
|
+
return carried !== undefined && carried.ok ? carried.value : [];
|
|
157
|
+
},
|
|
158
|
+
presetScopeStatus: async () => {
|
|
159
|
+
const carried = await remote?.presetScopeStatus();
|
|
160
|
+
return carried !== undefined && carried.ok ? carried.value : undefined;
|
|
161
|
+
},
|
|
162
|
+
presetScopeRoster: async () => {
|
|
163
|
+
const carried = await remote?.presetScopeRoster();
|
|
164
|
+
return carried !== undefined && carried.ok ? carried.value : [];
|
|
165
|
+
},
|
|
166
|
+
presetScopeSet: async (name, presets) => {
|
|
167
|
+
const carried = await remote?.presetScopeSet({ name, presets });
|
|
168
|
+
invalidateModeFaces();
|
|
169
|
+
return carried !== undefined && carried.ok ? carried.value : { ok: false, error: 'remote absent' };
|
|
170
|
+
},
|
|
171
|
+
presetScopeAdopt: async (name, presets) => {
|
|
172
|
+
const carried = await remote?.presetScopeAdopt({ name, presets });
|
|
173
|
+
invalidateModeFaces();
|
|
174
|
+
return carried !== undefined && carried.ok ? carried.value : { ok: false, error: 'remote absent' };
|
|
175
|
+
},
|
|
176
|
+
presetScopeRelease: async (name) => {
|
|
177
|
+
const carried = await remote?.presetScopeRelease(name);
|
|
178
|
+
invalidateModeFaces();
|
|
179
|
+
return carried !== undefined && carried.ok ? carried.value : { ok: false, error: 'remote absent' };
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
void refresh();
|
|
183
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
184
|
+
name: 'settings.section',
|
|
185
|
+
id: 'capabilities',
|
|
186
|
+
// After the Plugins section (order 15).
|
|
187
|
+
order: 20,
|
|
188
|
+
label: () => t('section.nav'),
|
|
189
|
+
locale: NS,
|
|
190
|
+
inject: () => injected,
|
|
191
|
+
}, CapabilityCatalogCard));
|
|
192
|
+
return async () => {
|
|
193
|
+
for (const dispose of disposers.reverse())
|
|
194
|
+
await dispose();
|
|
195
|
+
};
|
|
196
|
+
}
|