dsh-skill-mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,537 @@
1
+ /**
2
+ * dsh-skill-mcp client half (v0.2.0).
3
+ *
4
+ * "管理中心"插件:单面板内 tab 切换「技能 / MCP」。
5
+ *
6
+ * - 通信:connection RPC(官方推荐的 Client→Host 私有通道),取代 v0.1 的
7
+ * /api/cc-skills/* HTTP 路由。信封 {ok, value | error{message}}。
8
+ * - 按项目 MCP:监测当前会话工作区 cwd(sessions.list store),变化即推送
9
+ * mcp/sync;host 热加载/热卸载对应 @deepseek-ai/dsh-mcp-client 条目。
10
+ * - 挂载点:设置页 settings.section + 侧边栏 footer 快捷入口(全屏浮层),
11
+ * 两处均渲染同一 CenterPanel。
12
+ */
13
+ window.__ModuleLoader__.load({
14
+ id: "dsh-skill-mcp",
15
+ factory: (require) => {
16
+ const React = require("react");
17
+ // 不用 jsx-runtime:某些 shell 构建里 require("react/jsx-runtime") 的命名导出
18
+ // 不可靠(jsx 为 undefined → "jsx is not a function"),createElement 最稳。
19
+ const jsx = (type, props) => React.createElement(type, props);
20
+
21
+ const INJECT = ["slots", "locale", "workspaces", "connection"];
22
+
23
+ /** connection RPC 通道(与 host 半一致)。 */
24
+ const RPC_CHANNEL = "/skill-mcp";
25
+
26
+ let hostCtx = null;
27
+
28
+ /**
29
+ * RPC 调用:解包 {ok, value | error} 信封,抛出传输级错误。
30
+ * 返回的 value 保留业务形状({ok, message, ...})。
31
+ */
32
+ async function callApi(endpoint, body = {}) {
33
+ const result = await hostCtx.connection.rpc.call(RPC_CHANNEL, endpoint, body);
34
+ if (result.ok) return result.value;
35
+ throw new Error(result.error?.message || `rpc ${endpoint} failed`);
36
+ }
37
+
38
+ // ------------------------------------------------------------------
39
+ // Styles
40
+ // ------------------------------------------------------------------
41
+
42
+ const css = `
43
+ .dsh-ccs-overlay { position: fixed; inset: 0; z-index: 1000; display: flex;
44
+ align-items: center; justify-content: center;
45
+ background: var(--dsw-alias-scrim, rgba(0,0,0,0.45)); }
46
+ .dsh-ccs-overlay-panel { width: 640px; max-width: 92vw; max-height: 86vh; overflow-y: auto;
47
+ background: var(--dsw-alias-bg-primary, #fff); color: var(--dsw-alias-text-primary, #222);
48
+ border-radius: 12px; padding: 20px 22px;
49
+ box-shadow: 0 8px 32px rgba(0,0,0,0.2); }
50
+ .dsh-ccs-title { margin: 0 0 6px; font-size: 16px; font-weight: 600; }
51
+ .dsh-ccs-tabs { display: flex; gap: 2px; margin: 8px 0 14px;
52
+ border-bottom: 1px solid var(--dsw-alias-border-primary, rgba(127,127,127,0.25)); }
53
+ .dsh-ccs-tab { appearance: none; padding: 7px 16px; background: transparent; border: none;
54
+ border-bottom: 2px solid transparent; margin-bottom: -1px; cursor: pointer; font-size: 13px;
55
+ color: var(--dsw-alias-text-secondary, #666); }
56
+ .dsh-ccs-tab:hover { color: var(--dsw-alias-text-primary, #222); }
57
+ .dsh-ccs-tab-active { color: var(--dsw-alias-text-link, #4b7bec); font-weight: 600;
58
+ border-bottom-color: var(--dsw-alias-text-link, #4b7bec); }
59
+ .dsh-ccs-root { font-size: 12px; color: var(--dsw-alias-text-secondary, #666);
60
+ word-break: break-all; margin-bottom: 10px; }
61
+ .dsh-ccs-list { display: flex; flex-direction: column; gap: 8px; }
62
+ .dsh-ccs-row { display: flex; align-items: center; gap: 10px;
63
+ padding: 10px 12px; border-radius: 8px;
64
+ background: var(--dsw-alias-bg-secondary, rgba(127,127,127,0.07)); }
65
+ .dsh-ccs-main { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; }
66
+ .dsh-ccs-text { flex: 1; min-width: 0; }
67
+ .dsh-ccs-name { font-size: 13px; font-weight: 600; white-space: nowrap;
68
+ overflow: hidden; text-overflow: ellipsis;
69
+ display: flex; align-items: center; gap: 6px; }
70
+ .dsh-ccs-sub { font-size: 12px; color: var(--dsw-alias-text-secondary, #666);
71
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
72
+ .dsh-ccs-badges { display: inline-flex; gap: 4px; flex-shrink: 0; }
73
+ .dsh-ccs-badge { font-size: 10px; padding: 1px 6px; border-radius: 8px; font-weight: 400;
74
+ background: var(--dsw-alias-bg-secondary, rgba(127,127,127,0.15));
75
+ color: var(--dsw-alias-text-secondary, #666); }
76
+ .dsh-ccs-badge-linked { background: var(--dsw-alias-state-info-bg, rgba(75,123,236,0.15));
77
+ color: var(--dsw-alias-text-link, #4b7bec); }
78
+ .dsh-ccs-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
79
+ .dsh-ccs-dot-ok { background: var(--dsw-alias-state-success-label, #2e7d32); }
80
+ .dsh-ccs-dot-loading { background: var(--dsw-alias-state-warning-label, #b7791f); }
81
+ .dsh-ccs-dot-error { background: var(--dsw-alias-state-error-label, #e5484d); }
82
+ .dsh-ccs-dot-off { background: var(--dsw-alias-state-inactive-label, #9e9e9e); }
83
+ .dsh-ccs-empty { padding: 18px; text-align: center; font-size: 12px;
84
+ color: var(--dsw-alias-text-secondary, #666); border: 1px dashed var(--dsw-alias-border-primary, rgba(127,127,127,0.3));
85
+ border-radius: 8px; }
86
+ .dsh-ccs-actions { display: flex; gap: 8px; margin-top: 14px; flex-wrap: wrap; }
87
+ .dsh-ccs-msg { font-size: 12px; margin-top: 10px; white-space: pre-wrap; word-break: break-all; }
88
+ .dsh-ccs-msg-ok { color: var(--dsw-alias-state-success-label, #2e7d32); }
89
+ .dsh-ccs-msg-err { color: var(--dsw-alias-state-error-label, #e5484d); }
90
+ .dsh-ccs-fileinfo { font-size: 11px; line-height: 1.8; color: var(--dsw-alias-text-secondary, #666);
91
+ margin: 0 0 12px; word-break: break-all; }
92
+ .dsh-ccs-switch { appearance: none; width: 34px; height: 18px; border-radius: 9px; cursor: pointer;
93
+ background: var(--dsw-alias-state-inactive-label, #9e9e9e); position: relative;
94
+ transition: background 0.15s; flex-shrink: 0; outline: none; }
95
+ .dsh-ccs-switch:checked { background: var(--dsw-alias-state-success-label, #2e7d32); }
96
+ .dsh-ccs-switch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px;
97
+ border-radius: 50%; background: #fff; transition: transform 0.15s; }
98
+ .dsh-ccs-switch:checked::after { transform: translateX(16px); }
99
+ .dsh-ccs-switch:disabled { opacity: 0.5; cursor: not-allowed; }
100
+ .dsh-ccs-btn { appearance: none; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 12px;
101
+ background: var(--dsw-alias-bg-secondary, rgba(127,127,127,0.12)); color: var(--dsw-alias-text-primary, #222);
102
+ border: 1px solid var(--dsw-alias-border-primary, rgba(127,127,127,0.3)); }
103
+ .dsh-ccs-btn:disabled { opacity: 0.5; cursor: not-allowed; }
104
+ .dsh-ccs-btn-primary { background: var(--dsw-alias-state-info-bg, rgba(75,123,236,0.15));
105
+ border-color: var(--dsw-alias-text-link, #4b7bec); color: var(--dsw-alias-text-link, #4b7bec); }
106
+ .dsh-ccs-btn-danger { color: var(--dsh-ccs-danger, #e5484d); border-color: var(--dsw-alias-state-error-label, #e5484d); }
107
+ .dsh-ccs-icon-btn { appearance: none; background: none; border: none; cursor: pointer; font-size: 13px;
108
+ color: var(--dsw-alias-text-secondary, #666); padding: 2px 4px; border-radius: 4px; }
109
+ .dsh-ccs-icon-btn:hover { background: var(--dsw-alias-bg-secondary, rgba(127,127,127,0.12)); }
110
+ .dsh-ccs-icon-btn-danger:hover { color: var(--dsh-ccs-danger, #e5484d); }
111
+ .dsh-ccs-input { flex: 1; min-width: 0; padding: 7px 10px; border-radius: 6px; font-size: 12px;
112
+ border: 1px solid var(--dsw-alias-border-primary, rgba(127,127,127,0.35));
113
+ background: var(--dsw-alias-bg-primary, transparent); color: var(--dsw-alias-text-primary, #222); }
114
+ .dsh-ccs-input-row { display: flex; gap: 8px; align-items: center; margin: 12px 0; }
115
+ `;
116
+
117
+ function ensureStyles() {
118
+ if (document.getElementById('dsh-ccs-style')) return;
119
+ const el = document.createElement('style');
120
+ el.id = 'dsh-ccs-style';
121
+ el.textContent = css;
122
+ document.head.appendChild(el);
123
+ }
124
+
125
+ // ------------------------------------------------------------------
126
+ // Shared bits
127
+ // ------------------------------------------------------------------
128
+
129
+ /** iOS 风格开关,加载即显示 checked,避免状态闪烁。 */
130
+ const Switch = ({ checked, onChange, disabled, title }) =>
131
+ jsx('input', {
132
+ type: 'checkbox', className: 'dsh-ccs-switch', checked,
133
+ onChange: (e) => onChange(e.target.checked), disabled, title,
134
+ });
135
+
136
+ // ------------------------------------------------------------------
137
+ // 技能与MCP:单面板 + tab(技能 / MCP)
138
+ // ------------------------------------------------------------------
139
+
140
+ function CenterPanel({ pickDirectory }) {
141
+ const [tab, setTab] = React.useState('skills');
142
+ return jsx('div', {
143
+ className: 'dsh-ccs-page',
144
+ children: [
145
+ jsx('h2', { key: 'title', className: 'dsh-ccs-title', children: '技能与MCP' }),
146
+ jsx('div', {
147
+ key: 'tabs', className: 'dsh-ccs-tabs', children: [
148
+ jsx('button', {
149
+ key: 'skills', type: 'button',
150
+ className: `dsh-ccs-tab${tab === 'skills' ? ' dsh-ccs-tab-active' : ''}`,
151
+ onClick: () => setTab('skills'), children: '技能',
152
+ }),
153
+ jsx('button', {
154
+ key: 'mcp', type: 'button',
155
+ className: `dsh-ccs-tab${tab === 'mcp' ? ' dsh-ccs-tab-active' : ''}`,
156
+ onClick: () => setTab('mcp'), children: 'MCP',
157
+ }),
158
+ ],
159
+ }),
160
+ tab === 'skills'
161
+ ? jsx(SkillManagerSection, { key: 'body', pickDirectory })
162
+ : jsx(McpPanel, { key: 'body' }),
163
+ ],
164
+ });
165
+ }
166
+
167
+ // ------------------------------------------------------------------
168
+ // MCP tab:按项目自动加载的 MCP 服务状态
169
+ // ------------------------------------------------------------------
170
+
171
+ const SOURCE_LABEL = {
172
+ user: '用户级',
173
+ 'project-dsh-json': '项目 .dsh',
174
+ 'project-mcp-json': '.mcp.json',
175
+ };
176
+
177
+ function mcpSummary(row) {
178
+ if (row.transport === 'streamable-http') return String(row.url || '');
179
+ const args = Array.isArray(row.args) && row.args.length > 0 ? ' ' + row.args.join(' ') : '';
180
+ return `${row.command ?? ''}${args}`.trim();
181
+ }
182
+
183
+ function mcpDotClass(row) {
184
+ if (row.disabled) return 'dsh-ccs-dot-off';
185
+ if (row.connected) return 'dsh-ccs-dot-ok';
186
+ if (row.fiberPhase === 'failed') return 'dsh-ccs-dot-error';
187
+ if (row.fiberPhase === 'loading' || row.fiberPhase === 'pending') return 'dsh-ccs-dot-loading';
188
+ return 'dsh-ccs-dot-off';
189
+ }
190
+
191
+ function McpPanel() {
192
+ const [status, setStatus] = React.useState(null);
193
+ const [error, setError] = React.useState(null);
194
+ const [busy, setBusy] = React.useState(false);
195
+
196
+ const load = React.useCallback(async () => {
197
+ try {
198
+ setError(null);
199
+ setStatus(await callApi('mcp/status'));
200
+ } catch (e) {
201
+ setError(e?.message || String(e));
202
+ }
203
+ }, []);
204
+
205
+ React.useEffect(() => { void load(); }, [load]);
206
+
207
+ const toggle = async (row) => {
208
+ if (busy) return;
209
+ setBusy(true); setError(null);
210
+ try {
211
+ await callApi('mcp/setEnabled', { name: row.name, enabled: row.disabled });
212
+ // MCP 工具目录变了:让聊天侧的会话缓存失效并重取。
213
+ try { hostCtx?.emit?.('connection/reset'); } catch { /* 静默降级 */ }
214
+ await load();
215
+ } catch (e) {
216
+ setError(e?.message || String(e));
217
+ } finally {
218
+ setBusy(false);
219
+ }
220
+ };
221
+
222
+ const reload = async () => {
223
+ if (busy) return;
224
+ setBusy(true); setError(null);
225
+ try {
226
+ await callApi('mcp/reload');
227
+ await load();
228
+ } catch (e) {
229
+ setError(e?.message || String(e));
230
+ } finally {
231
+ setBusy(false);
232
+ }
233
+ };
234
+
235
+ const servers = Array.isArray(status?.servers) ? status.servers : [];
236
+ const files = status?.files ?? {};
237
+ const fileLines = ['user', 'project-dsh-json', 'project-mcp-json']
238
+ .filter((k) => files[k]?.path)
239
+ .map((k) => `${files[k].exists ? '✓' : '—'} ${files[k].path}`);
240
+
241
+ return jsx('div', {
242
+ children: [
243
+ jsx('div', {
244
+ key: 'hint', className: 'dsh-ccs-sub', style: { marginBottom: 8 },
245
+ children: '切换工作区后自动加载该项目声明的 MCP 服务;改动配置文件约 0.5 秒内生效。',
246
+ }),
247
+ status?.cwd
248
+ ? jsx('div', { key: 'cwd', className: 'dsh-ccs-root', children: `当前项目:${status.cwd}` })
249
+ : null,
250
+ fileLines.length > 0
251
+ ? jsx('div', {
252
+ key: 'files', className: 'dsh-ccs-fileinfo',
253
+ children: fileLines.join('\n'),
254
+ })
255
+ : null,
256
+ error
257
+ ? jsx('div', { key: 'err', className: 'dsh-ccs-msg dsh-ccs-msg-err', children: error })
258
+ : null,
259
+ servers.length === 0
260
+ ? jsx('div', {
261
+ key: 'empty', className: 'dsh-ccs-empty',
262
+ children: '当前项目没有配置任何 MCP 服务。\n在 ~/.dsh/mcp.json(全局)或项目根 .dsh/mcp.json / .mcp.json(按项目)中声明,例如:\n{ "mcpServers": { "codegraph": { "command": "codegraph", "args": ["serve", "--mcp"] } } }',
263
+ })
264
+ : jsx('div', {
265
+ key: 'list', className: 'dsh-ccs-list',
266
+ children: servers.map((row) => jsx('div', {
267
+ key: row.name, className: 'dsh-ccs-row', children: [
268
+ jsx('span', { key: 'dot', className: `dsh-ccs-dot ${mcpDotClass(row)}`, title: row.disabled ? '已停用' : row.fiberPhase ?? '未同步' }),
269
+ jsx('div', {
270
+ key: 'main', className: 'dsh-ccs-main', children: jsx('div', {
271
+ className: 'dsh-ccs-text', children: [
272
+ jsx('div', {
273
+ key: 'name', className: 'dsh-ccs-name', children: [
274
+ row.name,
275
+ jsx('span', {
276
+ key: 'badge', className: 'dsh-ccs-badge',
277
+ children: SOURCE_LABEL[row.source] ?? row.source,
278
+ }),
279
+ row.toolCount > 0
280
+ ? jsx('span', { key: 'tools', className: 'dsh-ccs-badge', children: `${row.toolCount} 工具` })
281
+ : null,
282
+ ],
283
+ }),
284
+ jsx('div', { key: 'sub', className: 'dsh-ccs-sub', children: mcpSummary(row) || row.transport }),
285
+ ],
286
+ }),
287
+ }),
288
+ jsx(Switch, {
289
+ key: 'sw', checked: !row.disabled, disabled: busy,
290
+ onChange: () => void toggle(row),
291
+ title: row.disabled ? '启用该 MCP 服务' : '停用该 MCP 服务(重启后保持)',
292
+ }),
293
+ ],
294
+ })),
295
+ }),
296
+ jsx('div', {
297
+ key: 'actions', className: 'dsh-ccs-actions', children: jsx('button', {
298
+ type: 'button', className: 'dsh-ccs-btn', disabled: busy, onClick: () => void reload(),
299
+ children: busy ? '处理中…' : '重新加载配置',
300
+ }),
301
+ }),
302
+ ],
303
+ });
304
+ }
305
+
306
+ // ------------------------------------------------------------------
307
+ // 技能 tab(v0.1 逻辑保留,动作改走 RPC)
308
+ // ------------------------------------------------------------------
309
+
310
+ function SkillManagerSection({ pickDirectory }) {
311
+ const [skills, setSkills] = React.useState([]);
312
+ const [root, setRoot] = React.useState(null);
313
+ const [msg, setMsg] = React.useState(null);
314
+ const [err, setErr] = React.useState(null);
315
+ const [busy, setBusy] = React.useState(false);
316
+ const [linkOpen, setLinkOpen] = React.useState(false);
317
+ const [sourceDir, setSourceDir] = React.useState('');
318
+
319
+ const load = React.useCallback(async () => {
320
+ try {
321
+ setErr(null);
322
+ const data = await callApi('skills/list');
323
+ setSkills(data.skills || []);
324
+ setRoot(data.root || null);
325
+ } catch (e) {
326
+ setErr(e?.message || String(e));
327
+ }
328
+ }, []);
329
+
330
+ React.useEffect(() => { void load(); }, [load]);
331
+
332
+ const run = async (action, body) => {
333
+ setBusy(true); setMsg(null); setErr(null);
334
+ try {
335
+ const data = await callApi(action, body || {});
336
+ if (!data.ok) throw new Error(data.error);
337
+ setMsg(data.message);
338
+ // 技能目录变了:发 connection/reset 让聊天框的 '/' 技能选择器
339
+ // 清掉按会话缓存的目录并重拉(各监听方只做缓存清理/重取,可安全复用)。
340
+ try { hostCtx?.emit?.('connection/reset'); } catch { /* 宿主事件不可用时静默降级 */ }
341
+ await load();
342
+ } catch (e) {
343
+ setErr(e?.message || String(e));
344
+ } finally {
345
+ setBusy(false);
346
+ }
347
+ };
348
+
349
+ const openPicker = async () => {
350
+ try {
351
+ const picked = await pickDirectory();
352
+ if (typeof picked === 'string' && picked !== '') {
353
+ setSourceDir(picked);
354
+ setLinkOpen(true);
355
+ }
356
+ } catch {
357
+ setLinkOpen(true);
358
+ }
359
+ };
360
+
361
+ return jsx(React.Fragment, {
362
+ children: [
363
+ root
364
+ ? jsx('div', { key: 'root', className: 'dsh-ccs-root', children: `技能根目录:${root}` })
365
+ : null,
366
+ err
367
+ ? jsx('div', { key: 'err', className: 'dsh-ccs-msg dsh-ccs-msg-err', children: err })
368
+ : null,
369
+ msg
370
+ ? jsx('div', { key: 'msg', className: 'dsh-ccs-msg dsh-ccs-msg-ok', children: msg })
371
+ : null,
372
+ skills.length === 0
373
+ ? jsx('div', {
374
+ key: 'empty', className: 'dsh-ccs-empty',
375
+ children: '还没有任何技能。从其它目录链接技能进来,或直接把技能目录放进根目录。',
376
+ })
377
+ : jsx('div', {
378
+ key: 'list', className: 'dsh-ccs-list',
379
+ children: skills.map((s) => jsx('div', {
380
+ key: s.name, className: 'dsh-ccs-row', children: jsx('div', {
381
+ className: 'dsh-ccs-main', children: [
382
+ jsx('div', {
383
+ key: 'text', className: 'dsh-ccs-text', children: [
384
+ jsx('div', {
385
+ key: 'name', className: 'dsh-ccs-name', children: [
386
+ s.name,
387
+ jsx('span', {
388
+ key: 'badges', className: 'dsh-ccs-badges', children: [
389
+ s.agent_created ? jsx('span', { key: 'a', className: 'dsh-ccs-badge', children: 'agent' }) : null,
390
+ s.pinned ? jsx('span', { key: 'p', className: 'dsh-ccs-badge', children: '置顶' }) : null,
391
+ s.linked ? jsx('span', { key: 'l', className: 'dsh-ccs-badge dsh-ccs-badge-linked', children: '已链接' }) : null,
392
+ ],
393
+ }),
394
+ ],
395
+ }),
396
+ jsx('div', { key: 'desc', className: 'dsh-ccs-sub', children: s.description || s.target || '' }),
397
+ ],
398
+ }),
399
+ jsx(Switch, {
400
+ key: 'sw', checked: !s.disabled, disabled: busy || s.layout === 'file',
401
+ onChange: (on) => void run(on ? 'skills/enable' : 'skills/disable', { name: s.name }),
402
+ title: s.layout === 'file' ? '单文件技能请手动编辑' : on_off_title(s),
403
+ }),
404
+ jsx('button', {
405
+ key: 'unlink', type: 'button', className: 'dsh-ccs-icon-btn', disabled: busy,
406
+ onClick: () => void run('skills/unlink', { name: s.name }),
407
+ title: '移除联接点(保留源目录)', children: '断开',
408
+ }),
409
+ jsx('button', {
410
+ key: 'del', type: 'button', className: 'dsh-ccs-icon-btn dsh-ccs-icon-btn-danger', disabled: busy,
411
+ onClick: () => { if (confirm(`确定删除技能 '${s.name}'?\n仅 agent 创建且非置顶的技能可删除。`)) void run('skills/delete', { name: s.name }); },
412
+ title: '删除技能目录(仅 agent 创建)', children: '删除',
413
+ }),
414
+ ],
415
+ }),
416
+ })),
417
+ }),
418
+ linkOpen
419
+ ? jsx('div', {
420
+ key: 'link', className: 'dsh-ccs-input-row', children: [
421
+ jsx('input', {
422
+ key: 'input', className: 'dsh-ccs-input', value: sourceDir,
423
+ onChange: (e) => setSourceDir(e.target.value),
424
+ placeholder: '技能源目录(其下每个子目录视为一个技能)',
425
+ }),
426
+ jsx('button', {
427
+ key: 'go', type: 'button', className: 'dsh-ccs-btn dsh-ccs-btn-primary', disabled: busy || !sourceDir,
428
+ onClick: () => void run('skills/link', { sourceDirectory: sourceDir }),
429
+ children: '链接',
430
+ }),
431
+ ],
432
+ })
433
+ : null,
434
+ jsx('div', {
435
+ key: 'actions', className: 'dsh-ccs-actions', children: [
436
+ jsx('button', {
437
+ key: 'pick', type: 'button', className: 'dsh-ccs-btn dsh-ccs-btn-primary', disabled: busy,
438
+ onClick: () => void openPicker(), children: '选择目录链接…',
439
+ }),
440
+ jsx('button', {
441
+ key: 'refresh', type: 'button', className: 'dsh-ccs-btn', disabled: busy,
442
+ onClick: () => void load(), children: busy ? '处理中…' : '刷新',
443
+ }),
444
+ ],
445
+ }),
446
+ ],
447
+ });
448
+ }
449
+
450
+ const on_off_title = (s) => (s.disabled ? '启用(模型目录 + 聊天框选择器)' : '停用(模型目录 + 聊天框选择器都隐藏)');
451
+
452
+ // ------------------------------------------------------------------
453
+ // 按项目 MCP:cwd 上报
454
+ // ------------------------------------------------------------------
455
+
456
+ /**
457
+ * 订阅当前会话列表:cwd 变化即推送 mcp/sync。
458
+ * subscribe 不可用时退化为 2 秒轮询兜底。
459
+ */
460
+ function setupProjectMcpSync(ctx) {
461
+ let lastCwd = null;
462
+ const tick = () => {
463
+ let cwd;
464
+ try {
465
+ const snap = ctx.sessions?.list?.getSnapshot?.();
466
+ const id = snap?.current;
467
+ cwd = id !== undefined ? snap?.byId?.[id]?.cwd : undefined;
468
+ } catch { return; }
469
+ if (typeof cwd !== 'string' || cwd === '' || cwd === lastCwd) return;
470
+ lastCwd = cwd;
471
+ void callApi('mcp/sync', { cwd })
472
+ .catch((e) => console.warn('[dsh-skill-mcp] mcp sync failed:', e?.message || e));
473
+ };
474
+ tick();
475
+ let disposeSub = null;
476
+ try { disposeSub = ctx.sessions.list.subscribe(tick); } catch { /* store 不存在 */ }
477
+ const timer = setInterval(tick, 2000);
478
+ ctx.effect(() => () => {
479
+ clearInterval(timer);
480
+ if (typeof disposeSub === 'function') disposeSub();
481
+ });
482
+ }
483
+
484
+ /** 侧边栏 footer ⚡ 快捷入口:点击打开技能与MCP全屏浮层。 */
485
+ function SidebarLauncher() {
486
+ const [open, setOpen] = React.useState(false);
487
+ return jsx(React.Fragment, {
488
+ children: [
489
+ jsx('button', {
490
+ key: 'btn', type: 'button', className: 'dsh-ccs-icon-btn',
491
+ title: '技能与MCP', onClick: () => setOpen(true), children: '⚡',
492
+ }),
493
+ open
494
+ ? jsx('div', {
495
+ key: 'overlay', className: 'dsh-ccs-overlay',
496
+ onClick: (e) => { if (e.target === e.currentTarget) setOpen(false); },
497
+ children: jsx('div', {
498
+ className: 'dsh-ccs-overlay-panel',
499
+ children: jsx(CenterPanel, { pickDirectory: hostCtx?.workspaces?.pickDirectory }),
500
+ }),
501
+ })
502
+ : null,
503
+ ],
504
+ });
505
+ }
506
+
507
+ // ------------------------------------------------------------------
508
+ // apply
509
+ // ------------------------------------------------------------------
510
+
511
+ const apply = (ctx) => {
512
+ hostCtx = ctx;
513
+ ensureStyles();
514
+ ctx.effect(() => () => { hostCtx = null; });
515
+
516
+ setupProjectMcpSync(ctx);
517
+
518
+ // 设置页:技能与MCP(settings.section 槽位)
519
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
520
+ name: 'settings.section',
521
+ id: 'dsh-skill-mcp',
522
+ order: 99,
523
+ label: () => '技能与MCP',
524
+ }, (props) => jsx(CenterPanel, { ...props, pickDirectory: ctx.workspaces?.pickDirectory })));
525
+
526
+ // 侧边栏 footer ⚡ 快捷入口(sidebar.footer.action 槽位)
527
+ ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
528
+ name: 'sidebar.footer.action',
529
+ id: 'dsh-skill-mcp',
530
+ order: 30,
531
+ label: () => '技能与MCP',
532
+ }, SidebarLauncher));
533
+ };
534
+
535
+ return { apply, inject: INJECT };
536
+ },
537
+ });