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