dsh-skill-mcp 0.2.0 → 0.4.1

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 (3) hide show
  1. package/lib/client.js +840 -154
  2. package/lib/index.js +412 -1
  3. package/package.json +2 -2
package/lib/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * dsh-skill-mcp client half (v0.2.0).
2
+ * dsh-skill-mcp client half (v0.3.0).
3
3
  *
4
4
  * "管理中心"插件:单面板内 tab 切换「技能 / MCP」。
5
5
  *
@@ -7,8 +7,10 @@
7
7
  * /api/cc-skills/* HTTP 路由。信封 {ok, value | error{message}}。
8
8
  * - 按项目 MCP:监测当前会话工作区 cwd(sessions.list store),变化即推送
9
9
  * mcp/sync;host 热加载/热卸载对应 @deepseek-ai/dsh-mcp-client 条目。
10
- * - 挂载点:设置页 settings.section + 侧边栏 footer 快捷入口(全屏浮层),
11
- * 两处均渲染同一 CenterPanel。
10
+ * - MCP 管理(v0.3):添加/编辑(表单 + JSON 切换)/删除(二次确认),
11
+ * 从其他 agent(Claude Code / Codex / Cursor / Gemini CLI)扫描导入,
12
+ * 写入作用域显式选择(全局 / 当前项目)。
13
+ * - 挂载点:设置页 settings.section 槽位,渲染 CenterPanel。
12
14
  */
13
15
  window.__ModuleLoader__.load({
14
16
  id: "dsh-skill-mcp",
@@ -40,15 +42,9 @@ window.__ModuleLoader__.load({
40
42
  // ------------------------------------------------------------------
41
43
 
42
44
  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;
45
+ .dsh-ccs-page { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
46
+ .dsh-ccs-title { margin: 0 0 6px; font-size: 16px; font-weight: 600; flex-shrink: 0; }
47
+ .dsh-ccs-tabs { display: flex; gap: 2px; margin: 8px 0 14px; flex-shrink: 0;
52
48
  border-bottom: 1px solid var(--dsw-alias-border-primary, rgba(127,127,127,0.25)); }
53
49
  .dsh-ccs-tab { appearance: none; padding: 7px 16px; background: transparent; border: none;
54
50
  border-bottom: 2px solid transparent; margin-bottom: -1px; cursor: pointer; font-size: 13px;
@@ -62,6 +58,7 @@ window.__ModuleLoader__.load({
62
58
  .dsh-ccs-row { display: flex; align-items: center; gap: 10px;
63
59
  padding: 10px 12px; border-radius: 8px;
64
60
  background: var(--dsw-alias-bg-secondary, rgba(127,127,127,0.07)); }
61
+ .dsh-ccs-row-disabled { opacity: 0.55; }
65
62
  .dsh-ccs-main { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; }
66
63
  .dsh-ccs-text { flex: 1; min-width: 0; }
67
64
  .dsh-ccs-name { font-size: 13px; font-weight: 600; white-space: nowrap;
@@ -83,6 +80,24 @@ window.__ModuleLoader__.load({
83
80
  .dsh-ccs-empty { padding: 18px; text-align: center; font-size: 12px;
84
81
  color: var(--dsw-alias-text-secondary, #666); border: 1px dashed var(--dsw-alias-border-primary, rgba(127,127,127,0.3));
85
82
  border-radius: 8px; }
83
+ /* 技能页头部:根目录信息 + 动作按钮(置顶) */
84
+ .dsh-ccs-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
85
+ margin-bottom: 10px; flex-shrink: 0; }
86
+ .dsh-ccs-head .dsh-ccs-root { flex: 1 1 auto; min-width: 0; margin-bottom: 0; }
87
+ .dsh-ccs-head-actions { display: flex; gap: 8px; flex-shrink: 0; }
88
+ .dsh-ccs-search-row { margin-bottom: 10px; flex-shrink: 0; }
89
+ .dsh-ccs-search { width: 100%; box-sizing: border-box; }
90
+ /* 列表区独立滚动:flex 填充 + 上限兜底(settings 页内无外层高度约束时用) */
91
+ .dsh-ccs-list-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; max-height: 52vh; }
92
+ .dsh-ccs-list-scroll::-webkit-scrollbar { width: 8px; }
93
+ .dsh-ccs-list-scroll::-webkit-scrollbar-thumb { background: var(--dsw-alias-scrollbar-bg-l2, rgba(127,127,127,0.35)); border-radius: 4px; }
94
+ .dsh-ccs-body-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; max-height: 60vh; }
95
+ .dsh-ccs-body-scroll::-webkit-scrollbar { width: 8px; }
96
+ .dsh-ccs-body-scroll::-webkit-scrollbar-thumb { background: var(--dsw-alias-scrollbar-bg-l2, rgba(127,127,127,0.35)); border-radius: 4px; }
97
+ /* 设置页上下文:区块撑满设置内容区剩余高度,消除底部大片留白 */
98
+ .dsh-ccs-page-fill { flex: 1 0 auto; min-height: calc(100vh - 180px); }
99
+ .dsh-ccs-page-fill .dsh-ccs-list-scroll,
100
+ .dsh-ccs-page-fill .dsh-ccs-body-scroll { max-height: none; }
86
101
  .dsh-ccs-actions { display: flex; gap: 8px; margin-top: 14px; flex-wrap: wrap; }
87
102
  .dsh-ccs-msg { font-size: 12px; margin-top: 10px; white-space: pre-wrap; word-break: break-all; }
88
103
  .dsh-ccs-msg-ok { color: var(--dsw-alias-state-success-label, #2e7d32); }
@@ -112,6 +127,140 @@ window.__ModuleLoader__.load({
112
127
  border: 1px solid var(--dsw-alias-border-primary, rgba(127,127,127,0.35));
113
128
  background: var(--dsw-alias-bg-primary, transparent); color: var(--dsw-alias-text-primary, #222); }
114
129
  .dsh-ccs-input-row { display: flex; gap: 8px; align-items: center; margin: 12px 0; }
130
+ /* 弹框:添加/编辑 MCP(遮罩用 bg-mask-1,表面用实底 bg-base,避免透出变暗) */
131
+ .dsh-ccs-modal-mask { position: fixed; inset: 0; z-index: 1100; display: flex;
132
+ align-items: center; justify-content: center; padding: 24px;
133
+ background: var(--dsw-alias-bg-mask-1, rgba(0,0,0,0.45)); }
134
+ .dsh-ccs-modal { display: flex; flex-direction: column; min-width: 0; box-sizing: border-box;
135
+ width: min(600px, 100%); max-height: min(80vh, 680px); overflow: auto; padding: 14px 16px;
136
+ border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.3)); border-radius: 12px;
137
+ background: var(--dsw-alias-bg-base, #fff); box-shadow: 0 12px 40px rgba(0,0,0,0.22); }
138
+ .dsh-ccs-modal .dsh-ccs-card { border: none; background: none; padding: 0; border-radius: 0; }
139
+ /* ===== MCP 页:对齐 dsh-plugin-capabilities 的 dpc- 设计语言 =====
140
+ 走宿主 --dsw-alias-label-*/-bg-layer-*/-border-l2/-state-* tokens,
141
+ 深浅主题自适应;共享类用 .dsh-ccs-mcp 作用域覆盖,不影响技能页。 */
142
+ .dsh-ccs-mcp { display: flex; flex-direction: column; gap: 12px; width: 100%;
143
+ max-width: 760px; color: var(--dsw-alias-label-primary, #222); }
144
+ .dsh-ccs-mcp .dsh-ccs-sub { color: var(--dsw-alias-label-tertiary, #8a8a8a); }
145
+ .dsh-ccs-mcp .dsh-ccs-root { margin: 0; color: var(--dsw-alias-label-secondary, #555); }
146
+ .dsh-ccs-mcp .dsh-ccs-fileinfo { margin: 0; font-size: 11px; line-height: 1.8;
147
+ color: var(--dsw-alias-label-tertiary, #8a8a8a); word-break: break-all; }
148
+ /* MCP 工具栏:添加 / 导入 / 重新加载 */
149
+ .dsh-ccs-toolbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
150
+ flex-shrink: 0; }
151
+ /* 列表头:标题 + 计数(同 dpc-listHead) */
152
+ .dsh-ccs-listhead { display: flex; align-items: baseline; gap: 7px; padding: 0 2px; }
153
+ .dsh-ccs-listhead-title { margin: 0; font-size: 13px; line-height: 20px; font-weight: 600; }
154
+ .dsh-ccs-listhead-count { font-size: 12px; line-height: 18px;
155
+ color: var(--dsw-alias-label-tertiary, #8a8a8a); font-variant-numeric: tabular-nums; }
156
+ /* 服务卡片:双列网格(同 dpc-cards/dpc-card) */
157
+ .dsh-ccs-cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
158
+ align-items: stretch; gap: 10px; }
159
+ @media (max-width: 680px) { .dsh-ccs-cards { grid-template-columns: minmax(0, 1fr); } }
160
+ .dsh-ccs-mcard { display: flex; flex-direction: column; gap: 6px; min-width: 0;
161
+ border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.3)); border-radius: 10px;
162
+ background: var(--dsw-alias-bg-layer-3, rgba(127,127,127,0.06)); padding: 10px 12px;
163
+ transition: background 0.15s; }
164
+ .dsh-ccs-mcard:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,0.1)); }
165
+ .dsh-ccs-mcard-off { opacity: 0.6; }
166
+ .dsh-ccs-mcard-top { display: flex; align-items: center; gap: 8px; }
167
+ .dsh-ccs-mcard-name { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
168
+ white-space: nowrap; font-size: 14px; line-height: 20px; font-weight: 600;
169
+ font-family: var(--ds-font-family-code, ui-monospace, Consolas, monospace); }
170
+ .dsh-ccs-mcard-spacer { flex: 1; }
171
+ .dsh-ccs-mcard-desc { font-size: 12px; line-height: 18px;
172
+ color: var(--dsw-alias-label-secondary, #555);
173
+ font-family: var(--ds-font-family-code, ui-monospace, Consolas, monospace);
174
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
175
+ .dsh-ccs-mcard-acts { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
176
+ /* 标签(同 dpc-tag):source 业务色、off 警示色 */
177
+ .dsh-ccs-tag { display: inline-flex; align-items: center; min-height: 18px; flex: none;
178
+ border-radius: 5px; padding: 1px 6px; background: var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.14));
179
+ color: var(--dsw-alias-label-secondary, #555); font-size: 11px; line-height: 16px; white-space: nowrap; }
180
+ .dsh-ccs-tag[data-kind='source'] { background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #4b7bec) 10%, transparent);
181
+ color: var(--dsw-alias-state-business-primary, #4b7bec); }
182
+ .dsh-ccs-tag[data-kind='off'] { background: color-mix(in srgb, var(--dsw-alias-state-warning-primary, #b7791f) 12%, transparent);
183
+ color: var(--dsw-alias-label-secondary, #555); }
184
+ /* MCP 消息条(同 dpc-banner):ok 绿 / err 红 tint */
185
+ .dsh-ccs-mcp .dsh-ccs-msg { margin: 0; display: flex; align-items: flex-start; gap: 8px;
186
+ border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.3)); border-radius: 8px;
187
+ padding: 9px 12px; background: var(--dsw-alias-bg-layer-3, rgba(127,127,127,0.06));
188
+ font-size: 12px; line-height: 18px; white-space: pre-wrap; word-break: break-all;
189
+ color: var(--dsw-alias-label-primary, #222); }
190
+ .dsh-ccs-mcp .dsh-ccs-msg-ok { border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary, #2e7d32) 35%, transparent);
191
+ background: color-mix(in srgb, var(--dsw-alias-state-success-primary, #2e7d32) 8%, transparent); }
192
+ .dsh-ccs-mcp .dsh-ccs-msg-err { border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary, #e5484d) 35%, transparent);
193
+ background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #e5484d) 8%, transparent);
194
+ color: var(--dsw-alias-state-error-primary, #e5484d); }
195
+ .dsh-ccs-mcp .dsh-ccs-empty { margin: 0; padding: 14px 2px; text-align: left;
196
+ font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-tertiary, #8a8a8a);
197
+ border: none; background: none; white-space: pre-wrap; }
198
+ /* MCP 按钮 / 输入框(作用域覆盖,不动技能页) */
199
+ .dsh-ccs-mcp .dsh-ccs-btn { appearance: none; padding: 5px 12px; border-radius: 8px;
200
+ cursor: pointer; font-size: 12px; line-height: 18px;
201
+ background: var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.12));
202
+ color: var(--dsw-alias-label-primary, #222);
203
+ border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.3)); }
204
+ .dsh-ccs-mcp .dsh-ccs-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,0.16)); }
205
+ .dsh-ccs-mcp .dsh-ccs-btn:disabled { opacity: 0.5; cursor: not-allowed; }
206
+ .dsh-ccs-mcp .dsh-ccs-btn-primary { background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #4b7bec) 14%, transparent);
207
+ border-color: color-mix(in srgb, var(--dsw-alias-state-business-primary, #4b7bec) 40%, transparent);
208
+ color: var(--dsw-alias-state-business-primary, #4b7bec); font-weight: 600; }
209
+ .dsh-ccs-mcp .dsh-ccs-btn-primary:hover:not(:disabled) { background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #4b7bec) 22%, transparent); }
210
+ .dsh-ccs-mcp .dsh-ccs-icon-btn { appearance: none; background: none; border: none; cursor: pointer;
211
+ font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary, #8a8a8a);
212
+ padding: 3px 8px; border-radius: 6px; }
213
+ .dsh-ccs-mcp .dsh-ccs-icon-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,0.12));
214
+ color: var(--dsw-alias-label-primary, #222); }
215
+ .dsh-ccs-mcp .dsh-ccs-icon-btn:disabled { opacity: 0.5; cursor: not-allowed; }
216
+ .dsh-ccs-mcp .dsh-ccs-icon-btn-danger:hover:not(:disabled) { background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #e5484d) 12%, transparent);
217
+ color: var(--dsw-alias-state-error-primary, #e5484d); }
218
+ .dsh-ccs-mcp .dsh-ccs-input, .dsh-ccs-mcp .dsh-ccs-select, .dsh-ccs-mcp .dsh-ccs-textarea {
219
+ border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.35)); border-radius: 8px;
220
+ background: var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.08));
221
+ color: var(--dsw-alias-label-primary, #222); outline: none; }
222
+ .dsh-ccs-mcp .dsh-ccs-input:focus-visible, .dsh-ccs-mcp .dsh-ccs-select:focus-visible,
223
+ .dsh-ccs-mcp .dsh-ccs-textarea:focus-visible { border-color: var(--dsw-alias-state-business-primary, #4b7bec);
224
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary, #4b7bec) 18%, transparent); }
225
+ .dsh-ccs-mcp .dsh-ccs-select:disabled { opacity: 0.5; cursor: not-allowed; }
226
+ /* MCP 添加/编辑表单与导入面板:内联展开的卡片 */
227
+ .dsh-ccs-card { display: flex; flex-direction: column; gap: 10px; padding: 12px 14px;
228
+ border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.3)); border-radius: 10px;
229
+ background: var(--dsw-alias-bg-layer-3, rgba(127,127,127,0.06)); flex-shrink: 0; }
230
+ .dsh-ccs-card-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
231
+ .dsh-ccs-card-title { font-size: 13px; line-height: 20px; font-weight: 600; flex: 1; min-width: 0; }
232
+ .dsh-ccs-json-toggle { display: inline-flex; align-items: center; gap: 5px; font-size: 12px;
233
+ color: var(--dsw-alias-label-tertiary, #8a8a8a); cursor: pointer; user-select: none; }
234
+ .dsh-ccs-json-toggle:hover { color: var(--dsw-alias-label-primary, #222); }
235
+ .dsh-ccs-form-row { display: flex; gap: 8px; align-items: center; }
236
+ .dsh-ccs-label { flex: none; width: 64px; font-size: 12px;
237
+ color: var(--dsw-alias-label-tertiary, #8a8a8a); text-align: right; }
238
+ .dsh-ccs-select { appearance: auto; padding: 6px 8px; border-radius: 8px; font-size: 12px;
239
+ cursor: pointer; min-width: 0; }
240
+ .dsh-ccs-textarea { width: 100%; box-sizing: border-box; min-height: 64px; padding: 7px 10px;
241
+ border-radius: 8px; font-size: 12px;
242
+ font-family: var(--ds-font-family-code, ui-monospace, Consolas, 'Courier New', monospace);
243
+ line-height: 1.5; resize: vertical; }
244
+ .dsh-ccs-textarea-tall { min-height: 120px; }
245
+ /* 导入面板:按 agent 分组的复选列表 */
246
+ .dsh-ccs-group { border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.3));
247
+ border-radius: 8px; overflow: hidden; margin-bottom: 8px; }
248
+ .dsh-ccs-group-head { display: flex; align-items: center; gap: 8px; padding: 7px 10px;
249
+ background: var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.08)); font-size: 12px; font-weight: 600;
250
+ color: var(--dsw-alias-label-primary, #222); cursor: pointer; user-select: none; }
251
+ .dsh-ccs-group-head:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,0.14)); }
252
+ .dsh-ccs-group-count { font-weight: 400; color: var(--dsw-alias-label-tertiary, #8a8a8a);
253
+ font-variant-numeric: tabular-nums; }
254
+ .dsh-ccs-check-row { display: flex; align-items: center; gap: 10px; padding: 7px 10px;
255
+ border-top: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.18));
256
+ font-size: 12px; color: var(--dsw-alias-label-primary, #222); cursor: pointer; }
257
+ .dsh-ccs-check-row:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,0.1)); }
258
+ .dsh-ccs-check-row-existing { opacity: 0.5; cursor: not-allowed; }
259
+ .dsh-ccs-check-name { font-weight: 600; flex: none;
260
+ font-family: var(--ds-font-family-code, ui-monospace, Consolas, monospace); }
261
+ .dsh-ccs-check-sub { flex: 1; min-width: 0; color: var(--dsw-alias-label-tertiary, #8a8a8a);
262
+ font-family: var(--ds-font-family-code, ui-monospace, Consolas, monospace);
263
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
115
264
  `;
116
265
 
117
266
  function ensureStyles() {
@@ -137,10 +286,10 @@ window.__ModuleLoader__.load({
137
286
  // 技能与MCP:单面板 + tab(技能 / MCP)
138
287
  // ------------------------------------------------------------------
139
288
 
140
- function CenterPanel({ pickDirectory }) {
289
+ function CenterPanel({ pickDirectory, fillHeight }) {
141
290
  const [tab, setTab] = React.useState('skills');
142
291
  return jsx('div', {
143
- className: 'dsh-ccs-page',
292
+ className: `dsh-ccs-page${fillHeight ? ' dsh-ccs-page-fill' : ''}`,
144
293
  children: [
145
294
  jsx('h2', { key: 'title', className: 'dsh-ccs-title', children: '技能与MCP' }),
146
295
  jsx('div', {
@@ -159,13 +308,16 @@ window.__ModuleLoader__.load({
159
308
  }),
160
309
  tab === 'skills'
161
310
  ? jsx(SkillManagerSection, { key: 'body', pickDirectory })
162
- : jsx(McpPanel, { key: 'body' }),
311
+ : jsx('div', {
312
+ key: 'body', className: 'dsh-ccs-body-scroll',
313
+ children: jsx(McpPanel),
314
+ }),
163
315
  ],
164
316
  });
165
317
  }
166
318
 
167
319
  // ------------------------------------------------------------------
168
- // MCP tab:按项目自动加载的 MCP 服务状态
320
+ // MCP tab:状态 + 添加/编辑/删除 + 从其他 agent 导入
169
321
  // ------------------------------------------------------------------
170
322
 
171
323
  const SOURCE_LABEL = {
@@ -174,6 +326,26 @@ window.__ModuleLoader__.load({
174
326
  'project-mcp-json': '.mcp.json',
175
327
  };
176
328
 
329
+ /** 编辑模式下 RPC source → 保存作用域(写回原文件)。 */
330
+ const SOURCE_TO_SCOPE = {
331
+ user: 'user',
332
+ 'project-dsh-json': 'project-dsh',
333
+ 'project-mcp-json': 'project-mcp',
334
+ };
335
+
336
+ const SCOPE_LABEL = {
337
+ user: '全局 ~/.dsh/mcp.json',
338
+ 'project-dsh': '当前项目 .dsh/mcp.json',
339
+ 'project-mcp': '当前项目 .mcp.json',
340
+ };
341
+
342
+ const AGENT_LABEL = {
343
+ 'claude-code': 'Claude Code',
344
+ codex: 'Codex',
345
+ cursor: 'Cursor',
346
+ gemini: 'Gemini CLI',
347
+ };
348
+
177
349
  function mcpSummary(row) {
178
350
  if (row.transport === 'streamable-http') return String(row.url || '');
179
351
  const args = Array.isArray(row.args) && row.args.length > 0 ? ' ' + row.args.join(' ') : '';
@@ -188,10 +360,371 @@ window.__ModuleLoader__.load({
188
360
  return 'dsh-ccs-dot-off';
189
361
  }
190
362
 
363
+ /** 每行 `KEY=VALUE` 或 `KEY: VALUE` 的文本 → 对象(# 开头跳过)。 */
364
+ function parseKvLines(text) {
365
+ const out = {};
366
+ for (const line of String(text || '').split('\n')) {
367
+ const t = line.trim();
368
+ if (t === '' || t.startsWith('#')) continue;
369
+ const m = t.match(/^([^=:]+?)\s*[=:]\s*(.*)$/);
370
+ if (m) out[m[1].trim()] = m[2].trim();
371
+ }
372
+ return out;
373
+ }
374
+
375
+ function kvToText(record) {
376
+ return Object.entries(record || {}).map(([k, v]) => `${k}=${v}`).join('\n');
377
+ }
378
+
379
+ function headersToText(record) {
380
+ return Object.entries(record || {}).map(([k, v]) => `${k}: ${v}`).join('\n');
381
+ }
382
+
383
+ function importSummary(server) {
384
+ if (server.transport === 'http') return String(server.url || '');
385
+ const args = Array.isArray(server.args) && server.args.length > 0 ? ' ' + server.args.join(' ') : '';
386
+ return `${server.command ?? ''}${args}`.trim();
387
+ }
388
+
389
+ /**
390
+ * 编辑器字段 → mcp/save payload。表单模式解析 KV / 参数文本;
391
+ * JSON 模式直接取原始条目字段。非法输入抛 Error(消息展示给用户)。
392
+ */
393
+ function editorToPayload(fields) {
394
+ if (fields.jsonMode) {
395
+ let raw;
396
+ try { raw = JSON.parse(fields.jsonText); } catch (e) { throw new Error(`JSON 解析失败:${e.message}`); }
397
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('JSON 条目必须是一个对象。');
398
+ const isHttp = raw.command === undefined || raw.command === null || raw.command === '';
399
+ return {
400
+ name: fields.name, scope: fields.scope,
401
+ transport: isHttp ? 'http' : 'stdio',
402
+ command: raw.command, args: raw.args, env: raw.env, cwd: raw.cwd,
403
+ url: raw.url, headers: raw.headers,
404
+ };
405
+ }
406
+ return {
407
+ name: fields.name.trim(), scope: fields.scope,
408
+ transport: fields.transport,
409
+ command: fields.command,
410
+ args: fields.transport === 'stdio' ? fields.args.trim().split(/\s+/).filter(Boolean) : [],
411
+ env: fields.transport === 'stdio' ? parseKvLines(fields.env) : {},
412
+ // cwd 不在表单里提供:host 统一按当前项目根兜底;需要固定目录请走 JSON 编辑(写 cwd 字段)。
413
+ url: fields.url,
414
+ headers: fields.transport === 'http' ? parseKvLines(fields.headers) : {},
415
+ };
416
+ }
417
+
418
+ /** 添加/编辑表单(含 JSON 编辑切换)。 */
419
+ function McpEditor({ editor, setEditor, hasProject, onSaved, setMsg, setErr, busy, setBusy }) {
420
+ const f = editor.fields;
421
+ const setField = (patch) => setEditor({ ...editor, fields: { ...f, ...patch } });
422
+
423
+ const save = async () => {
424
+ setBusy(true); setErr(null);
425
+ try {
426
+ const payload = editorToPayload(f);
427
+ const res = await callApi('mcp/save', payload);
428
+ if (!res.ok) throw new Error(res.error);
429
+ setMsg(res.message);
430
+ try { hostCtx?.emit?.('connection/reset'); } catch { /* 静默降级 */ }
431
+ await onSaved();
432
+ setEditor(null);
433
+ } catch (e) {
434
+ setErr(e?.message || String(e));
435
+ } finally {
436
+ setBusy(false);
437
+ }
438
+ };
439
+
440
+ const toggleJson = (on) => {
441
+ const jsonText = f.jsonText
442
+ || JSON.stringify(f.transport === 'http'
443
+ ? { type: 'http', url: 'https://example.com/mcp' }
444
+ : { command: 'npx', args: ['-y', 'package'] }, null, 2);
445
+ setField({ jsonMode: on, jsonText });
446
+ };
447
+
448
+ const rows = [];
449
+ if (editor.mode === 'add') {
450
+ rows.push(jsx('div', {
451
+ key: 'name', className: 'dsh-ccs-form-row', children: [
452
+ jsx('span', { key: 'l', className: 'dsh-ccs-label', children: '名称' }),
453
+ jsx('input', {
454
+ key: 'v', className: 'dsh-ccs-input', value: f.name,
455
+ onChange: (e) => setField({ name: e.target.value }),
456
+ placeholder: '1-32 位字母数字 _ -(如 codegraph)',
457
+ }),
458
+ ],
459
+ }));
460
+ }
461
+ if (!f.jsonMode) {
462
+ rows.push(jsx('div', {
463
+ key: 'transport', className: 'dsh-ccs-form-row', children: [
464
+ jsx('span', { key: 'l', className: 'dsh-ccs-label', children: '类型' }),
465
+ jsx('select', {
466
+ key: 'v', className: 'dsh-ccs-select', value: f.transport,
467
+ onChange: (e) => setField({ transport: e.target.value }),
468
+ children: [
469
+ jsx('option', { key: 'stdio', value: 'stdio', children: 'stdio(本地命令)' }),
470
+ jsx('option', { key: 'http', value: 'http', children: 'HTTP(远程服务)' }),
471
+ ],
472
+ }),
473
+ ],
474
+ }));
475
+ if (f.transport === 'stdio') {
476
+ rows.push(
477
+ jsx('div', {
478
+ key: 'command', className: 'dsh-ccs-form-row', children: [
479
+ jsx('span', { key: 'l', className: 'dsh-ccs-label', children: '命令' }),
480
+ jsx('input', {
481
+ key: 'v', className: 'dsh-ccs-input', value: f.command,
482
+ onChange: (e) => setField({ command: e.target.value }),
483
+ placeholder: '如 npx / codegraph / node',
484
+ }),
485
+ ],
486
+ }),
487
+ jsx('div', {
488
+ key: 'args', className: 'dsh-ccs-form-row', children: [
489
+ jsx('span', { key: 'l', className: 'dsh-ccs-label', children: '参数' }),
490
+ jsx('input', {
491
+ key: 'v', className: 'dsh-ccs-input', value: f.args,
492
+ onChange: (e) => setField({ args: e.target.value }),
493
+ placeholder: '空格分隔,如 serve --mcp',
494
+ }),
495
+ ],
496
+ }),
497
+ jsx('div', {
498
+ key: 'env', className: 'dsh-ccs-form-row', children: [
499
+ jsx('span', { key: 'l', className: 'dsh-ccs-label', children: '环境变量' }),
500
+ jsx('textarea', {
501
+ key: 'v', className: 'dsh-ccs-textarea', value: f.env,
502
+ onChange: (e) => setField({ env: e.target.value }),
503
+ placeholder: '每行一条,如 API_KEY=abc',
504
+ }),
505
+ ],
506
+ }),
507
+ );
508
+ } else {
509
+ rows.push(
510
+ jsx('div', {
511
+ key: 'url', className: 'dsh-ccs-form-row', children: [
512
+ jsx('span', { key: 'l', className: 'dsh-ccs-label', children: 'URL' }),
513
+ jsx('input', {
514
+ key: 'v', className: 'dsh-ccs-input', value: f.url,
515
+ onChange: (e) => setField({ url: e.target.value }),
516
+ placeholder: 'https://example.com/mcp',
517
+ }),
518
+ ],
519
+ }),
520
+ jsx('div', {
521
+ key: 'headers', className: 'dsh-ccs-form-row', children: [
522
+ jsx('span', { key: 'l', className: 'dsh-ccs-label', children: '请求头' }),
523
+ jsx('textarea', {
524
+ key: 'v', className: 'dsh-ccs-textarea', value: f.headers,
525
+ onChange: (e) => setField({ headers: e.target.value }),
526
+ placeholder: '每行一条,如 Authorization: Bearer xxx',
527
+ }),
528
+ ],
529
+ }),
530
+ );
531
+ }
532
+ } else {
533
+ rows.push(jsx('textarea', {
534
+ key: 'json', className: 'dsh-ccs-textarea dsh-ccs-textarea-tall', value: f.jsonText,
535
+ onChange: (e) => setField({ jsonText: e.target.value }),
536
+ spellCheck: false,
537
+ placeholder: '{ "command": "…", "args": […] } 或 { "type": "http", "url": "…" }',
538
+ }));
539
+ }
540
+ // 写入位置:添加时显式选择;编辑时锁定为来源文件。
541
+ rows.push(jsx('div', {
542
+ key: 'scope', className: 'dsh-ccs-form-row', children: [
543
+ jsx('span', { key: 'l', className: 'dsh-ccs-label', children: '写入位置' }),
544
+ editor.mode === 'add'
545
+ ? jsx('select', {
546
+ key: 'v', className: 'dsh-ccs-select', value: f.scope,
547
+ onChange: (e) => setField({ scope: e.target.value }),
548
+ children: [
549
+ jsx('option', { key: 'user', value: 'user', children: SCOPE_LABEL.user }),
550
+ jsx('option', {
551
+ key: 'project-dsh', value: 'project-dsh', disabled: !hasProject,
552
+ children: hasProject ? SCOPE_LABEL['project-dsh'] : `${SCOPE_LABEL['project-dsh']}(需先打开项目)`,
553
+ }),
554
+ ],
555
+ })
556
+ : jsx('span', { key: 'v', className: 'dsh-ccs-sub', children: SCOPE_LABEL[f.scope] ?? f.scope }),
557
+ ],
558
+ }));
559
+
560
+ return jsx('div', {
561
+ className: 'dsh-ccs-card',
562
+ children: [
563
+ jsx('div', {
564
+ key: 'head', className: 'dsh-ccs-card-head', children: [
565
+ jsx('span', {
566
+ key: 'title', className: 'dsh-ccs-card-title',
567
+ children: editor.mode === 'add' ? '添加 MCP 服务' : `编辑 MCP 服务:${editor.name}`,
568
+ }),
569
+ jsx('label', {
570
+ key: 'json', className: 'dsh-ccs-json-toggle', children: [
571
+ jsx('input', {
572
+ key: 'cb', type: 'checkbox', checked: f.jsonMode,
573
+ onChange: (e) => toggleJson(e.target.checked),
574
+ }),
575
+ 'JSON 编辑',
576
+ ],
577
+ }),
578
+ ],
579
+ }),
580
+ ...rows,
581
+ jsx('div', {
582
+ key: 'btns', className: 'dsh-ccs-form-row', children: [
583
+ jsx('button', {
584
+ key: 'save', type: 'button', className: 'dsh-ccs-btn dsh-ccs-btn-primary',
585
+ disabled: busy || (editor.mode === 'add' && !f.name.trim()),
586
+ onClick: () => void save(), children: busy ? '保存中…' : '保存',
587
+ }),
588
+ jsx('button', {
589
+ key: 'cancel', type: 'button', className: 'dsh-ccs-btn',
590
+ disabled: busy, onClick: () => setEditor(null), children: '取消',
591
+ }),
592
+ ],
593
+ }),
594
+ ],
595
+ });
596
+ }
597
+
598
+ /** 从其他 agent 导入:按 agent 分组的复选列表 + 作用域选择。 */
599
+ function McpImporter({ importer, setImporter, hasProject, onSaved, setMsg, setErr, busy, setBusy }) {
600
+ const toggleKey = (key, on) => {
601
+ const checked = new Set(importer.checked);
602
+ if (on) checked.add(key); else checked.delete(key);
603
+ setImporter({ ...importer, checked });
604
+ };
605
+ // 按来源 agent 分组(保持扫描顺序:claude-code / cursor / gemini / codex)。
606
+ const groups = [];
607
+ for (const s of importer.servers) {
608
+ let g = groups.find((x) => x.agent === s.agent);
609
+ if (!g) { g = { agent: s.agent, items: [] }; groups.push(g); }
610
+ g.items.push(s);
611
+ }
612
+
613
+ const doImport = async () => {
614
+ const items = importer.servers
615
+ .filter((s) => importer.checked.has(`${s.agent}/${s.name}`))
616
+ .map((s) => ({ agent: s.agent, name: s.name }));
617
+ if (items.length === 0) return;
618
+ setBusy(true); setErr(null);
619
+ try {
620
+ const res = await callApi('mcp/import/apply', { items, scope: importer.scope });
621
+ if (!res.ok) throw new Error(res.error);
622
+ setMsg(res.message);
623
+ setImporter({ ...importer, checked: new Set(), results: res.results });
624
+ try { hostCtx?.emit?.('connection/reset'); } catch { /* 静默降级 */ }
625
+ await onSaved();
626
+ } catch (e) {
627
+ setErr(e?.message || String(e));
628
+ } finally {
629
+ setBusy(false);
630
+ }
631
+ };
632
+
633
+ return jsx('div', {
634
+ className: 'dsh-ccs-card',
635
+ children: [
636
+ jsx('div', {
637
+ key: 'head', className: 'dsh-ccs-card-head', children: [
638
+ jsx('span', { key: 'title', className: 'dsh-ccs-card-title', children: '从其他 agent 导入 MCP' }),
639
+ jsx('select', {
640
+ key: 'scope', className: 'dsh-ccs-select', value: importer.scope,
641
+ onChange: (e) => setImporter({ ...importer, scope: e.target.value }),
642
+ children: [
643
+ jsx('option', { key: 'user', value: 'user', children: SCOPE_LABEL.user }),
644
+ jsx('option', {
645
+ key: 'project-dsh', value: 'project-dsh', disabled: !hasProject,
646
+ children: hasProject ? SCOPE_LABEL['project-dsh'] : `${SCOPE_LABEL['project-dsh']}(需先打开项目)`,
647
+ }),
648
+ ],
649
+ }),
650
+ jsx('button', {
651
+ key: 'close', type: 'button', className: 'dsh-ccs-btn', disabled: busy,
652
+ onClick: () => setImporter(null), children: '关闭',
653
+ }),
654
+ ],
655
+ }),
656
+ importer.loading
657
+ ? jsx('div', { key: 'loading', className: 'dsh-ccs-empty', children: '正在扫描其他 agent 的配置…' })
658
+ : null,
659
+ !importer.loading && importer.servers.length === 0
660
+ ? jsx('div', {
661
+ key: 'empty', className: 'dsh-ccs-empty',
662
+ children: '未在本机发现其他 agent 的 MCP 配置。\n支持:Claude Code(~/.claude.json)、Cursor(~/.cursor/mcp.json)、Codex(~/.codex/config.toml)、Gemini CLI(~/.gemini/settings.json)。',
663
+ })
664
+ : null,
665
+ ...groups.map((g) => {
666
+ const selectable = g.items.filter((s) => !importer.existing.has(s.name));
667
+ const allChecked = selectable.length > 0 && selectable.every((s) => importer.checked.has(`${s.agent}/${s.name}`));
668
+ return jsx('div', { key: g.agent, className: 'dsh-ccs-group', children: [
669
+ jsx('label', {
670
+ key: 'head', className: 'dsh-ccs-group-head', children: [
671
+ jsx('input', {
672
+ key: 'cb', type: 'checkbox', checked: allChecked, disabled: selectable.length === 0,
673
+ onChange: (e) => {
674
+ const checked = new Set(importer.checked);
675
+ for (const s of selectable) {
676
+ const key = `${s.agent}/${s.name}`;
677
+ if (e.target.checked) checked.add(key); else checked.delete(key);
678
+ }
679
+ setImporter({ ...importer, checked });
680
+ },
681
+ }),
682
+ `${AGENT_LABEL[g.agent] ?? g.agent}`,
683
+ jsx('span', { key: 'n', className: 'dsh-ccs-group-count', children: `(${g.items.length})` }),
684
+ ],
685
+ }),
686
+ ...g.items.map((s) => {
687
+ const key = `${s.agent}/${s.name}`;
688
+ const exists = importer.existing.has(s.name);
689
+ return jsx('label', {
690
+ key, className: `dsh-ccs-check-row${exists ? ' dsh-ccs-check-row-existing' : ''}`, children: [
691
+ jsx('input', {
692
+ key: 'cb', type: 'checkbox', checked: importer.checked.has(key), disabled: exists,
693
+ onChange: (e) => toggleKey(key, e.target.checked),
694
+ }),
695
+ jsx('span', { key: 'name', className: 'dsh-ccs-check-name', children: s.name }),
696
+ jsx('span', { key: 'sub', className: 'dsh-ccs-check-sub', children: importSummary(s) || s.transport }),
697
+ exists ? jsx('span', { key: 'badge', className: 'dsh-ccs-badge', children: '已存在' }) : null,
698
+ ],
699
+ });
700
+ }),
701
+ ] });
702
+ }),
703
+ Array.isArray(importer.results) && importer.results.length > 0
704
+ ? jsx('div', {
705
+ key: 'results', className: 'dsh-ccs-msg',
706
+ children: importer.results.map((r, i) => `${r.ok ? '✓' : '✗'} ${r.name}${r.error ? `:${r.error}` : ''}`).join('\n'),
707
+ })
708
+ : null,
709
+ !importer.loading && importer.servers.length > 0
710
+ ? jsx('div', {
711
+ key: 'btns', className: 'dsh-ccs-form-row', children: jsx('button', {
712
+ type: 'button', className: 'dsh-ccs-btn dsh-ccs-btn-primary', disabled: busy || importer.checked.size === 0,
713
+ onClick: () => void doImport(), children: busy ? '导入中…' : `导入所选(${importer.checked.size})`,
714
+ }),
715
+ })
716
+ : null,
717
+ ],
718
+ });
719
+ }
720
+
191
721
  function McpPanel() {
192
722
  const [status, setStatus] = React.useState(null);
193
723
  const [error, setError] = React.useState(null);
724
+ const [msg, setMsg] = React.useState(null);
194
725
  const [busy, setBusy] = React.useState(false);
726
+ const [editor, setEditor] = React.useState(null);
727
+ const [importer, setImporter] = React.useState(null);
195
728
 
196
729
  const load = React.useCallback(async () => {
197
730
  try {
@@ -206,7 +739,7 @@ window.__ModuleLoader__.load({
206
739
 
207
740
  const toggle = async (row) => {
208
741
  if (busy) return;
209
- setBusy(true); setError(null);
742
+ setBusy(true); setError(null); setMsg(null);
210
743
  try {
211
744
  await callApi('mcp/setEnabled', { name: row.name, enabled: row.disabled });
212
745
  // MCP 工具目录变了:让聊天侧的会话缓存失效并重取。
@@ -221,7 +754,7 @@ window.__ModuleLoader__.load({
221
754
 
222
755
  const reload = async () => {
223
756
  if (busy) return;
224
- setBusy(true); setError(null);
757
+ setBusy(true); setError(null); setMsg(null);
225
758
  try {
226
759
  await callApi('mcp/reload');
227
760
  await load();
@@ -232,16 +765,93 @@ window.__ModuleLoader__.load({
232
765
  }
233
766
  };
234
767
 
768
+ const openAdd = () => {
769
+ setError(null); setMsg(null);
770
+ setEditor({
771
+ mode: 'add', name: '', scope: 'user',
772
+ // name / scope 必须放 fields 里 —— 输入框、保存校验与 payload 都读 f.*。
773
+ fields: {
774
+ name: '', scope: 'user', transport: 'stdio', command: '', args: '', env: '', url: '', headers: '',
775
+ jsonMode: false, jsonText: '',
776
+ },
777
+ });
778
+ };
779
+
780
+ const openEdit = async (row) => {
781
+ setError(null); setMsg(null);
782
+ try {
783
+ const res = await callApi('mcp/get', { name: row.name });
784
+ if (!res.ok) throw new Error(res.error);
785
+ const raw = res.raw ?? {};
786
+ const isHttp = raw.command === undefined || raw.command === null || raw.command === '';
787
+ setEditor({
788
+ mode: 'edit', name: row.name, scope: SOURCE_TO_SCOPE[res.source] || 'user',
789
+ fields: {
790
+ name: row.name,
791
+ // 编辑模式锁定写回原来源文件(scope 必须在 fields 里才会随 payload 提交)。
792
+ scope: SOURCE_TO_SCOPE[res.source] || 'user',
793
+ transport: isHttp ? 'http' : 'stdio',
794
+ command: typeof raw.command === 'string' ? raw.command : '',
795
+ args: Array.isArray(raw.args) ? raw.args.join(' ') : '',
796
+ env: kvToText(raw.env),
797
+ url: typeof raw.url === 'string' ? raw.url : '',
798
+ headers: headersToText(raw.headers),
799
+ jsonMode: false,
800
+ jsonText: JSON.stringify(raw, null, 2),
801
+ },
802
+ });
803
+ } catch (e) {
804
+ setError(e?.message || String(e));
805
+ }
806
+ };
807
+
808
+ const removeRow = async (row) => {
809
+ const path = status?.files?.[row.source]?.path || SOURCE_LABEL[row.source] || row.source;
810
+ if (!confirm(`确定从 ${path} 中删除 MCP 服务 '${row.name}'?\n该操作会直接修改配置文件并卸载对应工具。`)) return;
811
+ setBusy(true); setError(null); setMsg(null);
812
+ try {
813
+ const res = await callApi('mcp/remove', { name: row.name });
814
+ if (!res.ok) throw new Error(res.error);
815
+ setMsg(res.message);
816
+ try { hostCtx?.emit?.('connection/reset'); } catch { /* 静默降级 */ }
817
+ await load();
818
+ } catch (e) {
819
+ setError(e?.message || String(e));
820
+ } finally {
821
+ setBusy(false);
822
+ }
823
+ };
824
+
825
+ const openImport = async () => {
826
+ setError(null); setMsg(null);
827
+ setImporter({ loading: true, servers: [], existing: new Set(), checked: new Set(), scope: 'user', results: null });
828
+ try {
829
+ const res = await callApi('mcp/import/scan');
830
+ if (!res.ok) throw new Error(res.error);
831
+ setImporter({
832
+ loading: false,
833
+ servers: Array.isArray(res.servers) ? res.servers : [],
834
+ existing: new Set(Array.isArray(res.existing) ? res.existing : []),
835
+ checked: new Set(), scope: 'user', results: null,
836
+ });
837
+ } catch (e) {
838
+ setError(e?.message || String(e));
839
+ setImporter(null);
840
+ }
841
+ };
842
+
235
843
  const servers = Array.isArray(status?.servers) ? status.servers : [];
236
844
  const files = status?.files ?? {};
845
+ const hasProject = Boolean(status?.cwd);
237
846
  const fileLines = ['user', 'project-dsh-json', 'project-mcp-json']
238
847
  .filter((k) => files[k]?.path)
239
848
  .map((k) => `${files[k].exists ? '✓' : '—'} ${files[k].path}`);
240
849
 
241
850
  return jsx('div', {
851
+ className: 'dsh-ccs-mcp',
242
852
  children: [
243
853
  jsx('div', {
244
- key: 'hint', className: 'dsh-ccs-sub', style: { marginBottom: 8 },
854
+ key: 'hint', className: 'dsh-ccs-sub', style: { marginBottom: 0 },
245
855
  children: '切换工作区后自动加载该项目声明的 MCP 服务;改动配置文件约 0.5 秒内生效。',
246
856
  }),
247
857
  status?.cwd
@@ -253,52 +863,107 @@ window.__ModuleLoader__.load({
253
863
  children: fileLines.join('\n'),
254
864
  })
255
865
  : null,
866
+ // 工具栏:添加 / 导入 / 重新加载
867
+ jsx('div', {
868
+ key: 'toolbar', className: 'dsh-ccs-toolbar', children: [
869
+ jsx('button', {
870
+ key: 'add', type: 'button', className: 'dsh-ccs-btn dsh-ccs-btn-primary',
871
+ disabled: busy || editor !== null, onClick: openAdd, children: '添加 MCP 服务…',
872
+ }),
873
+ jsx('button', {
874
+ key: 'import', type: 'button', className: 'dsh-ccs-btn',
875
+ disabled: busy || importer !== null, onClick: () => void openImport(), children: '从其他 agent 导入…',
876
+ }),
877
+ jsx('button', {
878
+ key: 'reload', type: 'button', className: 'dsh-ccs-btn',
879
+ disabled: busy, onClick: () => void reload(), children: busy ? '处理中…' : '重新加载配置',
880
+ }),
881
+ ],
882
+ }),
883
+ editor
884
+ ? jsx('div', {
885
+ key: 'editor', className: 'dsh-ccs-modal-mask',
886
+ // 点遮罩关闭(点在弹框本体上不关)
887
+ onClick: (e) => { if (e.target === e.currentTarget) setEditor(null); },
888
+ children: jsx('div', {
889
+ className: 'dsh-ccs-modal',
890
+ children: jsx(McpEditor, {
891
+ editor, setEditor, hasProject, onSaved: load,
892
+ setMsg, setErr: setError, busy, setBusy,
893
+ }),
894
+ }),
895
+ })
896
+ : null,
897
+ importer
898
+ ? jsx(McpImporter, {
899
+ key: 'importer', importer, setImporter, hasProject, onSaved: load,
900
+ setMsg, setErr: setError, busy, setBusy,
901
+ })
902
+ : null,
256
903
  error
257
904
  ? jsx('div', { key: 'err', className: 'dsh-ccs-msg dsh-ccs-msg-err', children: error })
258
905
  : null,
906
+ msg
907
+ ? jsx('div', { key: 'msg', className: 'dsh-ccs-msg dsh-ccs-msg-ok', children: msg })
908
+ : null,
259
909
  servers.length === 0
260
910
  ? jsx('div', {
261
911
  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"] } } }',
912
+ children: '当前项目没有配置任何 MCP 服务。\n点击上方「添加 MCP 服务」或「从其他 agent 导入」,也可直接在 ~/.dsh/mcp.json(全局)或项目根 .dsh/mcp.json / .mcp.json(按项目)中声明。',
263
913
  })
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
- ],
914
+ : [
915
+ // 列表头:标题 + 计数(同 dpc-listHead)
916
+ jsx('div', {
917
+ key: 'listhead', className: 'dsh-ccs-listhead', children: [
918
+ jsx('h3', { key: 't', className: 'dsh-ccs-listhead-title', children: 'MCP 服务' }),
919
+ jsx('span', { key: 'n', className: 'dsh-ccs-listhead-count', children: `${servers.length}` }),
920
+ ],
921
+ }),
922
+ // 服务卡片:双列网格(同 dpc-card:top / desc / acts)
923
+ jsx('div', {
924
+ key: 'list', className: 'dsh-ccs-cards',
925
+ children: servers.map((row) => jsx('div', {
926
+ key: row.name, className: `dsh-ccs-mcard${row.disabled ? ' dsh-ccs-mcard-off' : ''}`, children: [
927
+ jsx('div', {
928
+ key: 'top', className: 'dsh-ccs-mcard-top', children: [
929
+ jsx('span', { key: 'dot', className: `dsh-ccs-dot ${mcpDotClass(row)}`, title: row.disabled ? '已停用' : row.fiberPhase ?? '未同步' }),
930
+ jsx('span', { key: 'name', className: 'dsh-ccs-mcard-name', title: row.name, children: row.name }),
931
+ row.disabled ? jsx('span', { key: 'off', className: 'dsh-ccs-tag', 'data-kind': 'off', children: '已停用' }) : null,
932
+ jsx('span', { key: 'sp', className: 'dsh-ccs-mcard-spacer' }),
933
+ jsx(Switch, {
934
+ key: 'sw', checked: !row.disabled, disabled: busy,
935
+ onChange: () => void toggle(row),
936
+ title: row.disabled ? '启用该 MCP 服务' : '停用该 MCP 服务(重启后保持)',
283
937
  }),
284
- jsx('div', { key: 'sub', className: 'dsh-ccs-sub', children: mcpSummary(row) || row.transport }),
285
938
  ],
286
939
  }),
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
- }),
940
+ jsx('div', {
941
+ key: 'desc', className: 'dsh-ccs-mcard-desc', title: mcpSummary(row) || row.transport,
942
+ children: mcpSummary(row) || row.transport,
943
+ }),
944
+ jsx('div', {
945
+ key: 'acts', className: 'dsh-ccs-mcard-acts', children: [
946
+ jsx('span', { key: 'src', className: 'dsh-ccs-tag', 'data-kind': 'source', children: SOURCE_LABEL[row.source] ?? row.source }),
947
+ row.toolCount > 0
948
+ ? jsx('span', { key: 'tools', className: 'dsh-ccs-tag', children: `${row.toolCount} 工具` })
949
+ : null,
950
+ jsx('span', { key: 'sp', className: 'dsh-ccs-mcard-spacer' }),
951
+ jsx('button', {
952
+ key: 'edit', type: 'button', className: 'dsh-ccs-icon-btn', disabled: busy,
953
+ onClick: () => void openEdit(row),
954
+ title: '编辑该条目(表单 / JSON)', children: '编辑',
955
+ }),
956
+ jsx('button', {
957
+ key: 'del', type: 'button', className: 'dsh-ccs-icon-btn dsh-ccs-icon-btn-danger', disabled: busy,
958
+ onClick: () => void removeRow(row),
959
+ title: '从配置文件删除(需确认)', children: '删除',
960
+ }),
961
+ ],
962
+ }),
963
+ ],
964
+ })),
965
+ }),
966
+ ],
302
967
  ],
303
968
  });
304
969
  }
@@ -315,8 +980,10 @@ window.__ModuleLoader__.load({
315
980
  const [busy, setBusy] = React.useState(false);
316
981
  const [linkOpen, setLinkOpen] = React.useState(false);
317
982
  const [sourceDir, setSourceDir] = React.useState('');
983
+ const [query, setQuery] = React.useState('');
318
984
 
319
985
  const load = React.useCallback(async () => {
986
+ setBusy(true);
320
987
  try {
321
988
  setErr(null);
322
989
  const data = await callApi('skills/list');
@@ -324,6 +991,8 @@ window.__ModuleLoader__.load({
324
991
  setRoot(data.root || null);
325
992
  } catch (e) {
326
993
  setErr(e?.message || String(e));
994
+ } finally {
995
+ setBusy(false);
327
996
  }
328
997
  }, []);
329
998
 
@@ -348,73 +1017,50 @@ window.__ModuleLoader__.load({
348
1017
 
349
1018
  const openPicker = async () => {
350
1019
  try {
1020
+ // pickDirectory 依赖 this(内部用 this.api.host),不能以裸函数传递后直接调用。
1021
+ if (typeof pickDirectory !== 'function') throw new Error('宿主未提供目录选择器');
351
1022
  const picked = await pickDirectory();
352
1023
  if (typeof picked === 'string' && picked !== '') {
353
1024
  setSourceDir(picked);
354
1025
  setLinkOpen(true);
355
1026
  }
356
- } catch {
1027
+ // null = 用户取消,静默即可。
1028
+ } catch (e) {
1029
+ setErr(`无法打开目录选择器:${e?.message || e}。可手动粘贴路径后点击「链接」。`);
357
1030
  setLinkOpen(true);
358
1031
  }
359
1032
  };
360
1033
 
1034
+ // 搜索过滤:按名称 / 描述,大小写不敏感。
1035
+ const q = query.trim().toLowerCase();
1036
+ const filtered = q === ''
1037
+ ? skills
1038
+ : skills.filter((s) =>
1039
+ (s.name || '').toLowerCase().includes(q)
1040
+ || (s.description || '').toLowerCase().includes(q));
1041
+
361
1042
  return jsx(React.Fragment, {
362
1043
  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
- ],
1044
+ // 头部:根目录信息(左)+ 动作按钮(右),固定在列表上方
1045
+ jsx('div', {
1046
+ key: 'head', className: 'dsh-ccs-head', children: [
1047
+ root
1048
+ ? jsx('div', { key: 'root', className: 'dsh-ccs-root', children: `技能根目录:${root}` })
1049
+ : null,
1050
+ jsx('div', {
1051
+ key: 'acts', className: 'dsh-ccs-head-actions', children: [
1052
+ jsx('button', {
1053
+ key: 'pick', type: 'button', className: 'dsh-ccs-btn dsh-ccs-btn-primary', disabled: busy,
1054
+ onClick: () => void openPicker(), children: '选择目录链接…',
1055
+ }),
1056
+ jsx('button', {
1057
+ key: 'refresh', type: 'button', className: 'dsh-ccs-btn', disabled: busy,
1058
+ onClick: () => void load(), children: busy ? '处理中…' : '刷新',
415
1059
  }),
416
- })),
1060
+ ],
417
1061
  }),
1062
+ ],
1063
+ }),
418
1064
  linkOpen
419
1065
  ? jsx('div', {
420
1066
  key: 'link', className: 'dsh-ccs-input-row', children: [
@@ -431,18 +1077,78 @@ window.__ModuleLoader__.load({
431
1077
  ],
432
1078
  })
433
1079
  : null,
1080
+ err
1081
+ ? jsx('div', { key: 'err', className: 'dsh-ccs-msg dsh-ccs-msg-err', children: err })
1082
+ : null,
1083
+ msg
1084
+ ? jsx('div', { key: 'msg', className: 'dsh-ccs-msg dsh-ccs-msg-ok', children: msg })
1085
+ : null,
1086
+ // 搜索框
434
1087
  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
- ],
1088
+ key: 'search', className: 'dsh-ccs-search-row', children: jsx('input', {
1089
+ type: 'search', className: 'dsh-ccs-input dsh-ccs-search', value: query,
1090
+ onChange: (e) => setQuery(e.target.value),
1091
+ placeholder: '搜索技能(名称 / 描述)…',
1092
+ }),
445
1093
  }),
1094
+ // 列表区:独立滚动,不带动整页
1095
+ skills.length === 0
1096
+ ? jsx('div', {
1097
+ key: 'empty', className: 'dsh-ccs-empty',
1098
+ children: '还没有任何技能。从其它目录链接技能进来,或直接把技能目录放进根目录。',
1099
+ })
1100
+ : filtered.length === 0
1101
+ ? jsx('div', { key: 'nomatch', className: 'dsh-ccs-empty', children: '没有匹配的技能。' })
1102
+ : jsx('div', {
1103
+ key: 'scroll', className: 'dsh-ccs-list-scroll', children: jsx('div', {
1104
+ key: 'list', className: 'dsh-ccs-list',
1105
+ children: filtered.map((s) => jsx('div', {
1106
+ key: s.name, className: `dsh-ccs-row${s.disabled ? ' dsh-ccs-row-disabled' : ''}`, children: jsx('div', {
1107
+ className: 'dsh-ccs-main', children: [
1108
+ jsx('div', {
1109
+ key: 'text', className: 'dsh-ccs-text', children: [
1110
+ jsx('div', {
1111
+ key: 'name', className: 'dsh-ccs-name', children: [
1112
+ s.name,
1113
+ jsx('span', {
1114
+ key: 'badges', className: 'dsh-ccs-badges', children: [
1115
+ s.agent_created ? jsx('span', { key: 'a', className: 'dsh-ccs-badge', children: 'agent' }) : null,
1116
+ s.pinned ? jsx('span', { key: 'p', className: 'dsh-ccs-badge', children: '置顶' }) : null,
1117
+ s.linked ? jsx('span', { key: 'l', className: 'dsh-ccs-badge dsh-ccs-badge-linked', children: '已链接' }) : null,
1118
+ ],
1119
+ }),
1120
+ ],
1121
+ }),
1122
+ jsx('div', { key: 'desc', className: 'dsh-ccs-sub', children: s.description || s.target || '' }),
1123
+ ],
1124
+ }),
1125
+ jsx(Switch, {
1126
+ key: 'sw', checked: !s.disabled, disabled: busy || s.layout === 'file',
1127
+ onChange: (on) => void run(on ? 'skills/enable' : 'skills/disable', { name: s.name }),
1128
+ title: s.layout === 'file' ? '单文件技能请手动编辑' : on_off_title(s),
1129
+ }),
1130
+ // 断开与删除职责不同、守卫互补,都只在必然成功时显示:
1131
+ // 断开 = 仅移除联接点、保留源目录(junction 技能才有);
1132
+ // 删除 = 连源目录一起删(仅 agent 创建、非置顶、非联接)。
1133
+ s.linked
1134
+ ? jsx('button', {
1135
+ key: 'unlink', type: 'button', className: 'dsh-ccs-icon-btn', disabled: busy,
1136
+ onClick: () => void run('skills/unlink', { name: s.name }),
1137
+ title: '移除联接点(保留源目录)', children: '断开',
1138
+ })
1139
+ : null,
1140
+ s.agent_created && !s.pinned && !s.linked
1141
+ ? jsx('button', {
1142
+ key: 'del', type: 'button', className: 'dsh-ccs-icon-btn dsh-ccs-icon-btn-danger', disabled: busy,
1143
+ onClick: () => { if (confirm(`确定删除技能 '${s.name}'?\n仅 agent 创建且非置顶的技能可删除。`)) void run('skills/delete', { name: s.name }); },
1144
+ title: '删除技能目录(仅 agent 创建)', children: '删除',
1145
+ })
1146
+ : null,
1147
+ ],
1148
+ }),
1149
+ })),
1150
+ }),
1151
+ }),
446
1152
  ],
447
1153
  });
448
1154
  }
@@ -455,14 +1161,20 @@ window.__ModuleLoader__.load({
455
1161
 
456
1162
  /**
457
1163
  * 订阅当前会话列表:cwd 变化即推送 mcp/sync。
458
- * subscribe 不可用时退化为 2 秒轮询兜底。
1164
+ * sessions 服务通过 ctx.get('sessions') 获取(服务不在注入列表里,
1165
+ * ctx.sessions 永远是 undefined —— v0.2/v0.3.0 的静默失效根因)。
1166
+ * 快照形状见 SessionsPort:list.getSnapshot() → { ids, byId, current, phase },
1167
+ * 当前会话 cwd = byId[current].cwd。subscribe 不可用时退化为 2 秒轮询兜底。
459
1168
  */
460
1169
  function setupProjectMcpSync(ctx) {
461
1170
  let lastCwd = null;
1171
+ const sessionsOf = () => {
1172
+ try { return typeof ctx.get === 'function' ? ctx.get('sessions') : undefined; } catch { return undefined; }
1173
+ };
462
1174
  const tick = () => {
463
1175
  let cwd;
464
1176
  try {
465
- const snap = ctx.sessions?.list?.getSnapshot?.();
1177
+ const snap = sessionsOf()?.list?.getSnapshot?.();
466
1178
  const id = snap?.current;
467
1179
  cwd = id !== undefined ? snap?.byId?.[id]?.cwd : undefined;
468
1180
  } catch { return; }
@@ -473,7 +1185,7 @@ window.__ModuleLoader__.load({
473
1185
  };
474
1186
  tick();
475
1187
  let disposeSub = null;
476
- try { disposeSub = ctx.sessions.list.subscribe(tick); } catch { /* store 不存在 */ }
1188
+ try { disposeSub = sessionsOf()?.list?.subscribe?.(tick) ?? null; } catch { /* store 不存在 */ }
477
1189
  const timer = setInterval(tick, 2000);
478
1190
  ctx.effect(() => () => {
479
1191
  clearInterval(timer);
@@ -481,29 +1193,6 @@ window.__ModuleLoader__.load({
481
1193
  });
482
1194
  }
483
1195
 
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
1196
  // ------------------------------------------------------------------
508
1197
  // apply
509
1198
  // ------------------------------------------------------------------
@@ -521,15 +1210,12 @@ window.__ModuleLoader__.load({
521
1210
  id: 'dsh-skill-mcp',
522
1211
  order: 99,
523
1212
  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));
1213
+ }, (props) => jsx(CenterPanel, {
1214
+ ...props,
1215
+ fillHeight: true,
1216
+ // 注意要包一层箭头函数:方法内部依赖 this,摘下来传会丢绑定。
1217
+ pickDirectory: ctx.workspaces ? () => ctx.workspaces.pickDirectory() : undefined,
1218
+ })));
533
1219
  };
534
1220
 
535
1221
  return { apply, inject: INJECT };