dsh-skills-manager 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,382 @@
1
+ import { createElement as h, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
2
+ import Wrench from 'lucide-react/dist/esm/icons/wrench.mjs';
3
+ import Plus from 'lucide-react/dist/esm/icons/plus.mjs';
4
+ import RefreshCw from 'lucide-react/dist/esm/icons/refresh-cw.mjs';
5
+ import Search from 'lucide-react/dist/esm/icons/search.mjs';
6
+ import X from 'lucide-react/dist/esm/icons/x.mjs';
7
+ import Trash2 from 'lucide-react/dist/esm/icons/trash-2.mjs';
8
+ import Globe from 'lucide-react/dist/esm/icons/globe.mjs';
9
+ import FolderGit from 'lucide-react/dist/esm/icons/folder-git.mjs';
10
+ import GitBranch from 'lucide-react/dist/esm/icons/git-branch.mjs';
11
+ import PackageOpen from 'lucide-react/dist/esm/icons/package-open.mjs';
12
+ import Puzzle from 'lucide-react/dist/esm/icons/puzzle.mjs';
13
+ import AlertCircle from 'lucide-react/dist/esm/icons/alert-circle.mjs';
14
+ import Check from 'lucide-react/dist/esm/icons/check.mjs';
15
+ export const inject = ['connection', 'slots', 'workspaces', 'sessions'];
16
+ const ICON_SPIN = ['dsh-skill-manager-spin', 'to { transform: rotate(360deg); }'];
17
+ function ensureKeyframes() {
18
+ if (typeof document === 'undefined')
19
+ return;
20
+ if (document.getElementById(ICON_SPIN[0]))
21
+ return;
22
+ const style = document.createElement('style');
23
+ style.id = ICON_SPIN[0];
24
+ style.textContent = `
25
+ @keyframes ${ICON_SPIN[0]} { ${ICON_SPIN[1]} }
26
+ @container (max-width: 360px) {
27
+ .dsh-skill-manager-row { grid-template-columns: minmax(0, 1fr) !important; }
28
+ .dsh-skill-manager-row-icon { display: none !important; }
29
+ .dsh-skill-manager-row-actions { grid-column: 1 !important; justify-content: flex-start !important; }
30
+ }
31
+ `;
32
+ document.head.appendChild(style);
33
+ }
34
+ const spinnerStyle = { animation: `${ICON_SPIN[0]} 1s linear infinite`, flex: '0 0 auto' };
35
+ const monoStyle = { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace' };
36
+ const tokens = {
37
+ border: 'var(--border-subtle, rgba(148, 163, 184, .22))',
38
+ borderStrong: 'var(--border-strong, rgba(148, 163, 184, .38))',
39
+ accent: 'var(--fg-accent, #6366f1)',
40
+ success: 'var(--fg-success, #16a34a)',
41
+ danger: 'var(--fg-danger, #dc2626)',
42
+ secondary: 'var(--fg-secondary, #64748b)',
43
+ surfaceHover: 'var(--bg-hover, rgba(148, 163, 184, .08))',
44
+ surfaceActive: 'var(--bg-active, rgba(99, 102, 241, .12))',
45
+ };
46
+ const panelStyle = { containerType: 'inline-size', display: 'grid', gap: 18, width: '100%', minWidth: 0, maxWidth: 760, padding: '6px 0 36px' };
47
+ function buttonStyle(tone, disabled) {
48
+ const base = {
49
+ display: 'inline-flex', alignItems: 'center', gap: 6, minHeight: 32, padding: '0 12px',
50
+ borderRadius: 8, border: `1px solid transparent`, fontSize: 13, fontWeight: 500,
51
+ cursor: disabled ? 'default' : 'pointer', opacity: disabled ? 0.55 : 1, whiteSpace: 'nowrap',
52
+ transition: 'background .15s ease, border-color .15s ease',
53
+ };
54
+ if (tone === 'primary')
55
+ return { ...base, background: tokens.accent, color: 'var(--fg-on-accent, #fff)', border: 'none' };
56
+ if (tone === 'danger')
57
+ return { ...base, background: 'transparent', color: tokens.danger, border: `1px solid ${tokens.border}` };
58
+ return { ...base, background: 'transparent', color: 'inherit', border: `1px solid ${tokens.border}` };
59
+ }
60
+ const iconButtonStyle = {
61
+ display: 'grid', placeItems: 'center', width: 28, height: 28, padding: 0,
62
+ border: 'none', borderRadius: 7, background: 'transparent', color: tokens.secondary, cursor: 'pointer',
63
+ };
64
+ const inputStyle = {
65
+ width: '100%', minHeight: 34, padding: '0 10px 0 30px', borderRadius: 8,
66
+ border: `1px solid ${tokens.border}`, background: 'var(--bg-primary, transparent)', color: 'inherit',
67
+ fontSize: 13, outline: 'none', boxSizing: 'border-box',
68
+ };
69
+ const selectStyle = {
70
+ minHeight: 34, maxWidth: 300, padding: '0 8px', borderRadius: 8,
71
+ border: `1px solid ${tokens.border}`, background: 'var(--bg-primary, transparent)', color: 'inherit', fontSize: 13,
72
+ };
73
+ export function apply(ctx) {
74
+ const client = ctx;
75
+ const list = async (projectRoot) => {
76
+ const result = await client.connection.rpc.call('/skill-manager', 'list', projectRoot ? { projectRoot } : {});
77
+ if (!result.ok)
78
+ throw new Error(result.error.message);
79
+ return result.value;
80
+ };
81
+ const mutate = async (endpoint, payload) => {
82
+ const result = await client.connection.rpc.call('/skill-manager', endpoint, payload);
83
+ if (!result.ok)
84
+ throw new Error(result.error.message);
85
+ return result.value;
86
+ };
87
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
88
+ name: 'settings.section',
89
+ id: 'skills',
90
+ order: 35,
91
+ label: 'Skill 管理',
92
+ inject: () => ({ list, mutate, pickDirectory: () => ctx.workspaces.pickDirectory(), loopback: client.connection.isLoopback }),
93
+ }, SkillManagerSection));
94
+ ctx.slots.inject('conversation.input.left', () => ctx.slots.register({
95
+ name: 'conversation.input.left',
96
+ id: 'skill-manager',
97
+ order: 10,
98
+ inject: () => ({ list, mutate, sessions: ctx.sessions.list }),
99
+ }, ComposerSkillButton));
100
+ }
101
+ function SkillManagerSection(props) {
102
+ const { list, mutate, pickDirectory, loopback, useWorkspaces } = props;
103
+ const workspaces = useWorkspaces(state => state);
104
+ const [projectRoot, setProjectRoot] = useState(undefined);
105
+ const [catalog, setCatalog] = useState(null);
106
+ const [query, setQuery] = useState('');
107
+ const [busy, setBusy] = useState(false);
108
+ const [error, setError] = useState(null);
109
+ const [repositoryOpen, setRepositoryOpen] = useState(false);
110
+ const [repository, setRepository] = useState('');
111
+ useEffect(() => { ensureKeyframes(); }, []);
112
+ useEffect(() => {
113
+ if (projectRoot === undefined && workspaces.items.length > 0)
114
+ setProjectRoot(workspaces.items[0].path);
115
+ if (projectRoot !== undefined && !workspaces.items.some(item => item.path === projectRoot))
116
+ setProjectRoot(workspaces.items[0]?.path);
117
+ }, [projectRoot, workspaces.items]);
118
+ const refresh = useCallback(async () => {
119
+ setBusy(true);
120
+ setError(null);
121
+ try {
122
+ setCatalog(await list(projectRoot));
123
+ }
124
+ catch (reason) {
125
+ setError(messageOf(reason));
126
+ }
127
+ finally {
128
+ setBusy(false);
129
+ }
130
+ }, [list, projectRoot]);
131
+ useEffect(() => { void refresh(); }, [refresh]);
132
+ const run = useCallback(async (action) => {
133
+ setBusy(true);
134
+ setError(null);
135
+ try {
136
+ setCatalog(await action());
137
+ return true;
138
+ }
139
+ catch (reason) {
140
+ setError(messageOf(reason));
141
+ return false;
142
+ }
143
+ finally {
144
+ setBusy(false);
145
+ }
146
+ }, []);
147
+ const filtered = useMemo(() => {
148
+ const normalized = query.trim().toLocaleLowerCase();
149
+ return (catalog?.entries ?? []).filter(entry => normalized.length === 0 || `${entry.name} ${entry.description}`.toLocaleLowerCase().includes(normalized));
150
+ }, [catalog, query]);
151
+ const stats = useMemo(() => {
152
+ const entries = catalog?.entries ?? [];
153
+ return {
154
+ total: entries.length,
155
+ global: entries.filter(entry => entry.globalEnabled).length,
156
+ project: entries.filter(entry => entry.projectEnabled).length,
157
+ };
158
+ }, [catalog]);
159
+ const importSkill = async () => {
160
+ const path = await pickDirectory();
161
+ if (path)
162
+ await run(() => mutate('install', { path, projectRoot }));
163
+ };
164
+ const importRepository = async (event) => {
165
+ event.preventDefault();
166
+ const source = repository.trim();
167
+ if (!source)
168
+ return;
169
+ if (await run(() => mutate('install-remote', { repository: source, projectRoot }))) {
170
+ setRepository('');
171
+ setRepositoryOpen(false);
172
+ }
173
+ };
174
+ const updateSkill = async (entry) => {
175
+ let path;
176
+ const remote = entry.sourceType === 'git' || Boolean(entry.sourceUrl);
177
+ if (!entry.updateSupported || (entry.updateError && !remote)) {
178
+ path = await pickDirectory() ?? undefined;
179
+ if (!path)
180
+ return;
181
+ }
182
+ await run(() => mutate('update', { name: entry.name, path, projectRoot }));
183
+ };
184
+ const selectedWorkspace = workspaces.items.find(item => item.path === projectRoot);
185
+ return h('div', { style: panelStyle, 'aria-busy': busy }, h('header', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16, flexWrap: 'wrap' } }, h('div', { style: { display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 } }, h(Puzzle, { size: 20, 'aria-hidden': true }), h('h2', { style: { margin: 0, fontSize: 18, fontWeight: 600 } }, 'Skill 管理'), busy ? h(RefreshCw, { size: 14, 'aria-hidden': true, style: spinnerStyle }) : null), h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } }, h('button', { type: 'button', style: buttonStyle('ghost', busy), onClick: () => void run(() => mutate('check-updates', { projectRoot })), disabled: busy, title: '检查已记录来源是否有变化' }, h(RefreshCw, { size: 14, 'aria-hidden': true }), '检查更新'), h('button', { type: 'button', style: buttonStyle('ghost', busy || !loopback), onClick: () => setRepositoryOpen(value => !value), disabled: busy || !loopback, title: loopback ? '从 Git 仓库导入 skill' : '仅在本机连接时可导入' }, h(GitBranch, { size: 14, 'aria-hidden': true }), '仓库'), h('button', { type: 'button', style: buttonStyle('primary', busy || !loopback), onClick: importSkill, disabled: busy || !loopback, title: loopback ? '从本机目录导入 skill' : '仅在本机连接时可导入' }, h(Plus, { size: 14, 'aria-hidden': true }), '本机'))), h('p', { style: { margin: '-8px 0 0', fontSize: 13, color: tokens.secondary, lineHeight: 1.5 } }, '从本机目录或 Git 仓库导入 skill,并按「全局」或「项目」范围启用。'), repositoryOpen ? h('form', { onSubmit: importRepository, style: { display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' } }, h('input', {
186
+ type: 'text',
187
+ value: repository,
188
+ onChange: (event) => setRepository(event.currentTarget.value),
189
+ placeholder: 'owner/repo 或 Git URL',
190
+ 'aria-label': 'Git 仓库地址',
191
+ autoFocus: true,
192
+ disabled: busy,
193
+ style: { ...inputStyle, flex: '1 1 260px', minWidth: 0, paddingLeft: 10 },
194
+ }), h('button', { type: 'submit', style: buttonStyle('primary', busy || !repository.trim()), disabled: busy || !repository.trim() }, h(GitBranch, { size: 14, 'aria-hidden': true }), '导入'), h('button', { type: 'button', style: iconButtonStyle, onClick: () => setRepositoryOpen(false), disabled: busy, title: '关闭仓库导入', 'aria-label': '关闭仓库导入' }, h(X, { size: 15, 'aria-hidden': true }))) : null, h('div', { style: { display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'center' } }, h('div', { style: { position: 'relative', flex: '1 1 220px', display: 'flex', alignItems: 'center' } }, h(Search, { size: 14, 'aria-hidden': true, style: { position: 'absolute', left: 9, color: tokens.secondary, pointerEvents: 'none' } }), h('input', { type: 'search', value: query, placeholder: '搜索 skill…', 'aria-label': '搜索 skill', onChange: (event) => setQuery(event.currentTarget.value), style: inputStyle })), h('label', { style: { display: 'flex', flex: '1 1 220px', minWidth: 0, alignItems: 'center', gap: 8, flexWrap: 'wrap', fontSize: 13, color: tokens.secondary } }, '项目范围', h('select', { value: projectRoot ?? '', onChange: (event) => setProjectRoot(event.currentTarget.value || undefined), style: { ...selectStyle, width: '100%' } }, h('option', { value: '' }, '只看全局'), ...workspaces.items.map(workspace => h('option', { key: workspace.workspaceId, value: workspace.path }, workspace.title))))), h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } }, h(StatChip, { label: '已安装', value: stats.total }), h(StatChip, { label: '全局启用', value: stats.global, tone: 'accent' }), projectRoot !== undefined ? h(StatChip, { label: '项目启用', value: stats.project, tone: 'accent' }) : null), error ? h('div', { role: 'alert', style: { display: 'flex', gap: 8, alignItems: 'center', margin: 0, padding: '9px 12px', borderRadius: 8, fontSize: 13, color: tokens.danger, background: 'rgba(220, 38, 38, .08)', border: '1px solid rgba(220, 38, 38, .28)' } }, h(AlertCircle, { size: 15, 'aria-hidden': true }), error) : null, h('section', { style: { display: 'grid', width: '100%', minWidth: 0, gap: 8 }, 'aria-label': '已安装 skill' }, catalog === null
195
+ ? h('p', { style: { margin: 0, padding: '24px 0', textAlign: 'center', fontSize: 13, color: tokens.secondary } }, '正在读取…')
196
+ : filtered.length === 0
197
+ ? h(EmptyState, { hasAny: (catalog.entries.length ?? 0) > 0 })
198
+ : filtered.map(entry => h(SkillRow, {
199
+ key: entry.name,
200
+ entry,
201
+ projectLabel: selectedWorkspace?.title ?? '当前项目',
202
+ projectAvailable: projectRoot !== undefined,
203
+ busy,
204
+ onGlobalChange: enabled => void run(() => mutate('set-enabled', { name: entry.name, scope: 'global', projectRoot, enabled })),
205
+ onProjectChange: enabled => void run(() => mutate('set-enabled', { name: entry.name, scope: 'project', projectRoot, enabled })),
206
+ onUpdate: () => void updateSkill(entry),
207
+ onRemove: () => void run(() => mutate('remove', { name: entry.name, projectRoot })),
208
+ }))));
209
+ }
210
+ function StatChip(props) {
211
+ const active = (props.tone === 'accent') && props.value > 0;
212
+ return h('span', { style: { display: 'inline-flex', alignItems: 'baseline', gap: 6, padding: '4px 10px', borderRadius: 999, fontSize: 12, border: `1px solid ${active ? 'rgba(99, 102, 241, .4)' : tokens.border}`, color: active ? tokens.accent : tokens.secondary, background: active ? 'rgba(99, 102, 241, .08)' : 'transparent' } }, h('span', null, props.label), h('strong', { style: { fontSize: 13, fontWeight: 600 } }, String(props.value)));
213
+ }
214
+ function EmptyState(props) {
215
+ return h('div', { style: { display: 'grid', justifyItems: 'center', gap: 8, padding: '36px 16px', borderRadius: 10, border: `1px dashed ${tokens.borderStrong}`, color: tokens.secondary, textAlign: 'center' } }, h(PackageOpen, { size: 26, 'aria-hidden': true }), h('p', { style: { margin: 0, fontSize: 13, lineHeight: 1.5 } }, props.hasAny ? '没有匹配的 skill,试试调整搜索词。' : '还没有导入任何 skill。点击右上角「导入」,从本机目录开始。'));
216
+ }
217
+ function ComposerSkillButton(props) {
218
+ const { session, sessions, list, mutate } = props;
219
+ const snapshot = useSyncExternalStore(sessions.subscribe, sessions.getSnapshot, sessions.getSnapshot);
220
+ const projectRoot = snapshot.byId[session.sessionId]?.cwd;
221
+ const [open, setOpen] = useState(false);
222
+ const containerRef = useRef(null);
223
+ useEffect(() => {
224
+ if (!open)
225
+ return;
226
+ const onPointerDown = (event) => {
227
+ if (containerRef.current && event.target instanceof Node && !containerRef.current.contains(event.target))
228
+ setOpen(false);
229
+ };
230
+ const onKeyDown = (event) => { if (event.key === 'Escape')
231
+ setOpen(false); };
232
+ document.addEventListener('pointerdown', onPointerDown);
233
+ document.addEventListener('keydown', onKeyDown);
234
+ return () => {
235
+ document.removeEventListener('pointerdown', onPointerDown);
236
+ document.removeEventListener('keydown', onKeyDown);
237
+ };
238
+ }, [open]);
239
+ return h('div', { ref: containerRef, style: { position: 'relative', display: 'inline-flex', marginLeft: -25 } }, h('button', {
240
+ type: 'button',
241
+ title: projectRoot ? '配置当前项目的 Skill' : '当前会话没有工作区',
242
+ 'aria-label': '配置当前项目的 Skill',
243
+ 'aria-expanded': open,
244
+ disabled: !projectRoot,
245
+ onClick: () => setOpen(value => !value),
246
+ style: {
247
+ display: 'grid',
248
+ placeItems: 'center',
249
+ flex: '0 0 auto',
250
+ width: 28,
251
+ height: 28,
252
+ padding: 0,
253
+ border: 'none',
254
+ borderRadius: 999,
255
+ background: open ? 'var(--dsw-alias-bg-active, rgba(255, 255, 255, .16))' : 'var(--dsw-specific-selector, rgba(255, 255, 255, .08))',
256
+ color: 'var(--dsw-alias-label-primary, inherit)',
257
+ cursor: projectRoot ? 'pointer' : 'default',
258
+ opacity: projectRoot ? 1 : 0.5,
259
+ },
260
+ }, h(Wrench, { size: 16, 'aria-hidden': true })), open && projectRoot ? h(ProjectSkillPopover, { projectRoot, list, mutate, onClose: () => setOpen(false) }) : null);
261
+ }
262
+ function ProjectSkillPopover(props) {
263
+ const { projectRoot, list, mutate, onClose } = props;
264
+ const theme = currentThemeColors();
265
+ const [catalog, setCatalog] = useState(null);
266
+ const [busy, setBusy] = useState(false);
267
+ const [error, setError] = useState(null);
268
+ useEffect(() => { ensureKeyframes(); }, []);
269
+ const refresh = useCallback(async () => {
270
+ setBusy(true);
271
+ setError(null);
272
+ try {
273
+ setCatalog(await list(projectRoot));
274
+ }
275
+ catch (reason) {
276
+ setError(messageOf(reason));
277
+ }
278
+ finally {
279
+ setBusy(false);
280
+ }
281
+ }, [list, projectRoot]);
282
+ useEffect(() => { void refresh(); }, [refresh]);
283
+ const toggle = async (entry, enabled) => {
284
+ setBusy(true);
285
+ setError(null);
286
+ try {
287
+ setCatalog(await mutate('set-enabled', { name: entry.name, scope: 'project', projectRoot, enabled }));
288
+ }
289
+ catch (reason) {
290
+ setError(messageOf(reason));
291
+ }
292
+ finally {
293
+ setBusy(false);
294
+ }
295
+ };
296
+ const enabledCount = (catalog?.entries ?? []).filter(entry => entry.projectEnabled).length;
297
+ return h('section', { style: { position: 'absolute', zIndex: 50, left: 0, bottom: 40, width: 320, display: 'grid', gap: 6, padding: 12, border: `1px solid ${tokens.borderStrong}`, borderRadius: 12, background: theme.background, color: theme.color, boxShadow: '0 16px 40px rgba(0, 0, 0, .4)' }, 'aria-label': '当前项目 Skill' }, h('header', { style: { display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'center', paddingBottom: 4 } }, h('div', { style: { display: 'flex', alignItems: 'center', gap: 7, minWidth: 0 } }, h(FolderGit, { size: 14, 'aria-hidden': true, style: { color: tokens.secondary, flex: '0 0 auto' } }), h('strong', { style: { fontSize: 13, fontWeight: 600 } }, '项目 Skill'), h('span', { style: { fontSize: 12, color: tokens.secondary } }, `${enabledCount}/${catalog?.entries.length ?? 0} 已启用`)), h('button', { type: 'button', onClick: onClose, style: iconButtonStyle, 'aria-label': '关闭项目 Skill', title: '关闭' }, h(X, { size: 14, 'aria-hidden': true }))), error ? h('div', { role: 'alert', style: { display: 'flex', gap: 6, alignItems: 'center', margin: 0, padding: '7px 9px', borderRadius: 7, fontSize: 12, color: tokens.danger, background: 'rgba(220, 38, 38, .08)' } }, h(AlertCircle, { size: 13, 'aria-hidden': true }), error) : null, h('div', { style: { display: 'grid', gap: 1, maxHeight: 280, overflowY: 'auto', margin: '0 -4px', padding: '0 4px' } }, catalog === null
298
+ ? h('p', { style: { margin: 0, padding: '18px 0', textAlign: 'center', fontSize: 12, color: tokens.secondary } }, '正在读取…')
299
+ : catalog.entries.length === 0
300
+ ? h('p', { style: { margin: 0, padding: '18px 0', textAlign: 'center', fontSize: 12, color: tokens.secondary } }, '未导入 skill。请到「设置 → Skill 管理」导入。')
301
+ : catalog.entries.map(entry => h('label', { key: entry.name, style: { display: 'grid', gridTemplateColumns: '14px minmax(0, 1fr) 30px', gap: 8, alignItems: 'center', padding: '7px 6px', borderRadius: 7, fontSize: 13, cursor: busy ? 'default' : 'pointer' }, title: entry.description || entry.name }, h(Puzzle, { size: 13, 'aria-hidden': true, style: { color: entry.projectEnabled ? tokens.accent : tokens.secondary } }), h('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, entry.name), h(Toggle, { checked: entry.projectEnabled, disabled: busy, onChange: enabled => void toggle(entry, enabled), label: entry.name })))), busy ? h('div', { style: { display: 'flex', justifyContent: 'flex-end', paddingTop: 2 } }, h(RefreshCw, { size: 12, 'aria-hidden': true, style: spinnerStyle })) : null);
302
+ }
303
+ function SkillRow(props) {
304
+ const { entry, projectLabel, projectAvailable, busy, onGlobalChange, onProjectChange, onUpdate, onRemove } = props;
305
+ const [hover, setHover] = useState(false);
306
+ return h('article', {
307
+ className: 'dsh-skill-manager-row',
308
+ style: {
309
+ display: 'grid', gridTemplateColumns: '32px minmax(0, 1fr)', gap: 12, alignItems: 'center',
310
+ width: '100%', minWidth: 0, maxWidth: '100%', overflow: 'hidden', boxSizing: 'border-box',
311
+ padding: '12px 14px', borderRadius: 10, border: `1px solid ${hover ? tokens.borderStrong : tokens.border}`,
312
+ background: hover ? tokens.surfaceHover : 'transparent', transition: 'border-color .15s ease, background .15s ease',
313
+ },
314
+ onMouseEnter: () => setHover(true),
315
+ onMouseLeave: () => setHover(false),
316
+ }, h('div', { className: 'dsh-skill-manager-row-icon', style: { display: 'grid', placeItems: 'center', width: 32, height: 32, borderRadius: 8, color: entry.effectiveEnabled ? tokens.accent : tokens.secondary, background: entry.effectiveEnabled ? 'rgba(99, 102, 241, .1)' : 'rgba(148, 163, 184, .1)' } }, h(Puzzle, { size: 16, 'aria-hidden': true })), h('div', { style: { minWidth: 0, display: 'grid', gap: 4 } }, h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, minWidth: 0, flexWrap: 'wrap' } }, h('strong', { style: { ...monoStyle, fontSize: 13, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, entry.name), h(SourceBadge, { entry }), h(StatusBadge, { active: entry.globalEnabled, icon: Globe, label: '全局' }), projectAvailable ? h(StatusBadge, { active: entry.projectEnabled, icon: FolderGit, label: projectLabel }) : null, entry.updateAvailable ? h('span', { style: { flex: '0 0 auto', fontSize: 11, color: tokens.accent } }, '有更新') : null, entry.updateError ? h('span', { style: { flex: '0 0 auto', fontSize: 11, color: tokens.danger }, title: entry.updateError }, '来源异常') : null), entry.description ? h('p', { style: { margin: 0, color: tokens.secondary, lineHeight: 1.45, fontSize: 12.5, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' } }, entry.description) : null), h('div', { className: 'dsh-skill-manager-row-actions', style: { gridColumn: '1 / -1', display: 'flex', width: '100%', minWidth: 0, justifyContent: 'flex-end', alignItems: 'center', gap: 10, flexWrap: 'wrap' } }, h(Toggle, { checked: entry.globalEnabled, disabled: busy, onChange: onGlobalChange, label: `${entry.name} 全局开关`, title: '全局启用' }), projectAvailable ? h(Toggle, { checked: entry.projectEnabled, disabled: busy, onChange: onProjectChange, label: `${entry.name} 项目开关`, title: `${projectLabel} 启用` }) : null, h('button', { type: 'button', style: { ...iconButtonStyle, color: entry.updateAvailable ? tokens.accent : tokens.secondary }, disabled: busy, onClick: onUpdate, title: entry.updateSupported && !entry.updateError ? (entry.sourceType === 'git' ? '从远程仓库更新' : '从原始目录更新') : '选择来源目录并更新', 'aria-label': `更新 ${entry.name}` }, h(RefreshCw, { size: 15, 'aria-hidden': true })), h('button', { type: 'button', style: { ...iconButtonStyle, color: tokens.danger }, disabled: busy, onClick: onRemove, title: '从库中移除', 'aria-label': `移除 ${entry.name}` }, h(Trash2, { size: 15, 'aria-hidden': true }))));
317
+ }
318
+ function SourceBadge(props) {
319
+ const remote = props.entry.sourceType === 'git' || Boolean(props.entry.sourceUrl);
320
+ const local = props.entry.sourceType === 'local' || Boolean(props.entry.sourcePath);
321
+ const Icon = remote ? GitBranch : local ? FolderGit : AlertCircle;
322
+ const label = remote ? 'Git' : local ? '本机' : '未知';
323
+ const title = remote ? props.entry.sourceUrl : local ? props.entry.sourcePath : '旧版本导入,首次更新需重新选择来源';
324
+ return h('span', {
325
+ title,
326
+ style: {
327
+ display: 'inline-flex', alignItems: 'center', gap: 4, flex: '0 1 auto', maxWidth: '100%', overflow: 'hidden',
328
+ padding: '2px 6px', borderRadius: 999, fontSize: 11, lineHeight: '16px', whiteSpace: 'nowrap',
329
+ color: tokens.secondary, background: 'rgba(148, 163, 184, .09)',
330
+ },
331
+ }, h(Icon, { size: 11, 'aria-hidden': true }), label);
332
+ }
333
+ function StatusBadge(props) {
334
+ const Icon = props.icon;
335
+ return h('span', {
336
+ title: props.active ? `${props.label}已启用` : `${props.label}未启用`,
337
+ style: {
338
+ display: 'inline-flex', alignItems: 'center', gap: 4, flex: '0 1 auto', maxWidth: '100%', overflow: 'hidden',
339
+ padding: '2px 6px', borderRadius: 999, fontSize: 11, lineHeight: '16px', whiteSpace: 'nowrap',
340
+ color: props.active ? tokens.success : tokens.secondary,
341
+ background: props.active ? 'rgba(22, 163, 74, .1)' : 'rgba(148, 163, 184, .09)',
342
+ border: `1px solid ${props.active ? 'rgba(22, 163, 74, .3)' : 'transparent'}`,
343
+ },
344
+ }, h(Icon, { size: 11, 'aria-hidden': true }), props.active ? `${props.label}开` : `${props.label}关`);
345
+ }
346
+ function Toggle(props) {
347
+ const { checked, disabled, onChange, label, title } = props;
348
+ return h('button', {
349
+ type: 'button',
350
+ role: 'switch',
351
+ 'aria-checked': checked,
352
+ 'aria-label': label,
353
+ title,
354
+ disabled,
355
+ onClick: () => onChange(!checked),
356
+ style: {
357
+ position: 'relative', flex: '0 0 auto', width: 30, height: 18, padding: 0, border: 'none', borderRadius: 999,
358
+ background: checked ? tokens.accent : 'rgba(148, 163, 184, .35)',
359
+ cursor: disabled ? 'default' : 'pointer', opacity: disabled ? 0.55 : 1,
360
+ transition: 'background .15s ease',
361
+ },
362
+ }, h('span', {
363
+ 'aria-hidden': true,
364
+ style: {
365
+ position: 'absolute', top: 2, left: checked ? 14 : 2, width: 14, height: 14, borderRadius: 999,
366
+ background: '#fff', boxShadow: '0 1px 2px rgba(0, 0, 0, .3)', transition: 'left .15s ease',
367
+ display: 'grid', placeItems: 'center',
368
+ },
369
+ }, checked ? h(Check, { size: 10, 'aria-hidden': true }) : null));
370
+ }
371
+ function currentThemeColors() {
372
+ if (typeof document === 'undefined')
373
+ return { background: '#151517', color: '#f9fafb' };
374
+ const style = getComputedStyle(document.body);
375
+ return {
376
+ background: style.backgroundColor || '#151517',
377
+ color: style.color || '#f9fafb',
378
+ };
379
+ }
380
+ function messageOf(reason) {
381
+ return reason instanceof Error ? reason.message : String(reason);
382
+ }
@@ -0,0 +1,37 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import Schema from '@deepseek-ai/schemastery';
3
+ import type { SkillProvider, SkillProviderControl } from '@deepseek-ai/dsh-skill';
4
+ import type { SkillCatalog, SkillScope } from './types.js';
5
+ export declare const name = "dsh-skills-manager";
6
+ export declare const inject: string[];
7
+ export interface Config {
8
+ readonly storageDir?: string;
9
+ }
10
+ export declare const Config: Schema<Config>;
11
+ export declare class SkillManager {
12
+ private readonly ctx;
13
+ private readonly root;
14
+ private readonly library;
15
+ private readonly stateFile;
16
+ private state;
17
+ private loaded;
18
+ private writeQueue;
19
+ private readonly updateChecks;
20
+ private providerControl;
21
+ private filesystemProvider;
22
+ constructor(ctx: Context, config: Config);
23
+ provider(control: SkillProviderControl): SkillProvider;
24
+ catalog(projectRoot?: string): Promise<SkillCatalog>;
25
+ install(sourcePath: string, projectRoot?: string): Promise<SkillCatalog>;
26
+ installRemote(input: string, projectRoot?: string): Promise<SkillCatalog>;
27
+ private installSources;
28
+ checkUpdates(projectRoot?: string): Promise<SkillCatalog>;
29
+ update(name: string, sourcePath: string | undefined, projectRoot?: string): Promise<SkillCatalog>;
30
+ private replaceSkill;
31
+ setEnabled(name: string, scope: SkillScope, projectRoot: string | undefined, enabled: boolean): Promise<SkillCatalog>;
32
+ remove(name: string, projectRoot?: string): Promise<SkillCatalog>;
33
+ private load;
34
+ private persist;
35
+ private managedCandidate;
36
+ }
37
+ export declare function apply(ctx: Context, config: Config): void;