@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.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +286 -0
  4. package/README.zh.md +138 -0
  5. package/cordis.patch.yml +8 -0
  6. package/lib/client.js +10100 -0
  7. package/lib/index.js +4022 -0
  8. package/lib/invariant.js +25 -0
  9. package/lib/mcpConnector-C2TbkPBY.js +69 -0
  10. package/lib/typert.host.d.ts +3 -0
  11. package/lib/typert.host.js +1417 -0
  12. package/lib/typert.remote-client.d.ts +68 -0
  13. package/lib/typert.remote-client.js +1136 -0
  14. package/lib/types/capabilities.d.ts +103 -0
  15. package/lib/types/capabilities.js +116 -0
  16. package/lib/types/channels.d.ts +41 -0
  17. package/lib/types/channels.js +75 -0
  18. package/lib/types/client/AddMcpDialog.d.ts +13 -0
  19. package/lib/types/client/AddMcpDialog.js +79 -0
  20. package/lib/types/client/AddSkillModal.d.ts +17 -0
  21. package/lib/types/client/AddSkillModal.js +138 -0
  22. package/lib/types/client/BundleFileTree.d.ts +7 -0
  23. package/lib/types/client/BundleFileTree.js +74 -0
  24. package/lib/types/client/CapabilityCatalogCard.d.ts +57 -0
  25. package/lib/types/client/CapabilityCatalogCard.js +479 -0
  26. package/lib/types/client/CredentialField.d.ts +14 -0
  27. package/lib/types/client/CredentialField.js +9 -0
  28. package/lib/types/client/McpCards.d.ts +13 -0
  29. package/lib/types/client/McpCards.js +12 -0
  30. package/lib/types/client/McpServerManageModal.d.ts +18 -0
  31. package/lib/types/client/McpServerManageModal.js +63 -0
  32. package/lib/types/client/MetadataRow.d.ts +10 -0
  33. package/lib/types/client/MetadataRow.js +6 -0
  34. package/lib/types/client/ModalShell.d.ts +16 -0
  35. package/lib/types/client/ModalShell.js +27 -0
  36. package/lib/types/client/ModeChips.d.ts +38 -0
  37. package/lib/types/client/ModeChips.js +42 -0
  38. package/lib/types/client/SchemaView.d.ts +11 -0
  39. package/lib/types/client/SchemaView.js +143 -0
  40. package/lib/types/client/SkillCards.d.ts +28 -0
  41. package/lib/types/client/SkillCards.js +43 -0
  42. package/lib/types/client/SkillDetailModal.d.ts +69 -0
  43. package/lib/types/client/SkillDetailModal.js +167 -0
  44. package/lib/types/client/ToolCards.d.ts +28 -0
  45. package/lib/types/client/ToolCards.js +52 -0
  46. package/lib/types/client/ToolDetailModal.d.ts +12 -0
  47. package/lib/types/client/ToolDetailModal.js +29 -0
  48. package/lib/types/client/icons.d.ts +26 -0
  49. package/lib/types/client/icons.js +10 -0
  50. package/lib/types/client/index.d.ts +21 -0
  51. package/lib/types/client/index.js +196 -0
  52. package/lib/types/client/locales.d.ts +392 -0
  53. package/lib/types/client/locales.js +395 -0
  54. package/lib/types/client/mcp-model.d.ts +24 -0
  55. package/lib/types/client/mcp-model.js +87 -0
  56. package/lib/types/client/mode-model.d.ts +108 -0
  57. package/lib/types/client/mode-model.js +140 -0
  58. package/lib/types/client/preset-display.d.ts +21 -0
  59. package/lib/types/client/preset-display.js +20 -0
  60. package/lib/types/client/settle.d.ts +40 -0
  61. package/lib/types/client/settle.js +57 -0
  62. package/lib/types/client/slots.d.ts +86 -0
  63. package/lib/types/client/slots.js +1 -0
  64. package/lib/types/envHint.d.ts +22 -0
  65. package/lib/types/envHint.js +76 -0
  66. package/lib/types/import.d.ts +106 -0
  67. package/lib/types/import.js +635 -0
  68. package/lib/types/index.d.ts +216 -0
  69. package/lib/types/index.js +739 -0
  70. package/lib/types/invariant.d.ts +16 -0
  71. package/lib/types/invariant.js +23 -0
  72. package/lib/types/mcpConnector.d.ts +24 -0
  73. package/lib/types/mcpConnector.js +73 -0
  74. package/lib/types/mcpStore.d.ts +68 -0
  75. package/lib/types/mcpStore.js +235 -0
  76. package/lib/types/mcpTools.d.ts +45 -0
  77. package/lib/types/mcpTools.js +125 -0
  78. package/lib/types/mcps.d.ts +64 -0
  79. package/lib/types/mcps.js +157 -0
  80. package/lib/types/modes.d.ts +65 -0
  81. package/lib/types/modes.js +97 -0
  82. package/lib/types/namespace.d.ts +9 -0
  83. package/lib/types/namespace.js +8 -0
  84. package/lib/types/official-tools.d.ts +3 -0
  85. package/lib/types/official-tools.js +44 -0
  86. package/lib/types/preset-scope.d.ts +105 -0
  87. package/lib/types/preset-scope.js +117 -0
  88. package/lib/types/remote.d.ts +48 -0
  89. package/lib/types/remote.js +120 -0
  90. package/lib/types/scoped-delivery.d.ts +243 -0
  91. package/lib/types/scoped-delivery.js +686 -0
  92. package/lib/types/scoped-edits.d.ts +90 -0
  93. package/lib/types/scoped-edits.js +234 -0
  94. package/lib/types/settings.d.ts +28 -0
  95. package/lib/types/settings.js +28 -0
  96. package/lib/types/shellEnv.d.ts +65 -0
  97. package/lib/types/shellEnv.js +232 -0
  98. package/lib/types/skills.d.ts +103 -0
  99. package/lib/types/skills.js +303 -0
  100. package/lib/types/tool-origin.d.ts +29 -0
  101. package/lib/types/tool-origin.js +30 -0
  102. package/lib/types/types.d.ts +315 -0
  103. package/lib/types/types.js +8 -0
  104. package/lib/types/zip.d.ts +24 -0
  105. package/lib/types/zip.js +80 -0
  106. package/locale/en.json +6 -0
  107. package/locale/zh.json +6 -0
  108. package/package.json +119 -0
  109. 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
+ }