dsh-vscode-mode 0.1.35 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 dsh-better-sidebar 侧边栏与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
5
5
  "keywords": [
6
6
  "dsh",
@@ -22,6 +22,8 @@ export const CACHE_KEY = {
22
22
  search: 'edrv.search.v1.',
23
23
  /** 侧边栏提示已关闭标记(全局,无会话)。 */
24
24
  sideHint: 'edrv.side-hint-dismissed',
25
+ /** 「性能优化」页工作区栏目展开状态(workspaceFoldCache v1,全局不按会话)。 */
26
+ workspaceFold: 'edrv.ws-fold.v1.',
25
27
  } as const
26
28
 
27
29
  /** 按前缀拼会话键(侧边栏/编辑器等需要布局段时自行拼接后传入)。 */
@@ -0,0 +1,95 @@
1
+ /**
2
+ * dsh-vscode-mode client — 「性能优化」页工作区栏目展开状态持久化(纯函数,可单测)。
3
+ * 只持久化「已展开」的工作区键集合(默认全部折叠),对齐 explorerCache 的做法:
4
+ * 不缓存会话条目本身,条目由 edrv.perf.inventory 实时返回。
5
+ * 键前缀统一走 paths.ts PathConst(CACHE_KEY.workspaceFold)。
6
+ * 作者 ddj 2026-09-02
7
+ */
8
+ import { CACHE_KEY } from '../paths.js'
9
+
10
+ /** localStorage 键(全局,不按会话隔离)。 */
11
+ const STORE_KEY = CACHE_KEY.workspaceFold
12
+
13
+ /** 展开键容量上限:超出丢弃尾部,防止无限增长撑爆配额。 */
14
+ export const FOLD_CAP = 300
15
+
16
+ /**
17
+ * 归一化工作区键:仅去首尾空白(工作区键非文件系统路径,不做斜杠转换)。
18
+ * @author ddj 2026年09月02号
19
+ * @param key 工作区标识
20
+ * @returns 归一化键,空值返回 ''
21
+ */
22
+ export function normalizeFoldKey(key: string | undefined | null): string {
23
+ return String(key ?? '').trim()
24
+ }
25
+
26
+ /**
27
+ * 裁剪展开键集:去重、丢弃空值、超容量丢弃尾部(保持传入顺序)。
28
+ * @author ddj 2026年09月02号
29
+ * @param expanded 原始展开键集合
30
+ * @returns 规范化后的键数组
31
+ */
32
+ export function sanitizeFolded(expanded: unknown): string[] {
33
+ if (!Array.isArray(expanded)) return []
34
+ const seen = new Set<string>()
35
+ const out: string[] = []
36
+ for (const item of expanded) {
37
+ const key = normalizeFoldKey(typeof item === 'string' ? item : '')
38
+ if (!key || seen.has(key)) continue
39
+ seen.add(key)
40
+ out.push(key)
41
+ if (out.length >= FOLD_CAP) break
42
+ }
43
+ return out
44
+ }
45
+
46
+ /**
47
+ * 序列化展开状态(JSON,带 schema 版本号)。
48
+ * @author ddj 2026年09月02号
49
+ * @param expanded 已展开的项目键集合
50
+ * @returns JSON 字符串
51
+ */
52
+ export function serializeFold(expanded: string[]): string {
53
+ return JSON.stringify({ v: 1, expanded: sanitizeFolded(expanded) })
54
+ }
55
+
56
+ /**
57
+ * 解析缓存文本;损坏/格式不符 → 空集合。
58
+ * @author ddj 2026年09月02号
59
+ * @param text localStorage 原文
60
+ * @returns 已展开键集合
61
+ */
62
+ export function parseFold(text: string | null): string[] {
63
+ if (!text) return []
64
+ try {
65
+ const data = JSON.parse(text)
66
+ if (!data || typeof data !== 'object') return []
67
+ return sanitizeFolded(data.expanded)
68
+ } catch (error) {
69
+ return []
70
+ }
71
+ }
72
+
73
+ /**
74
+ * 读取展开状态(localStorage 不可用 → 空集合)。
75
+ * @author ddj 2026年09月02号
76
+ * @returns 已展开键集合
77
+ */
78
+ export function foldLoad(): string[] {
79
+ try {
80
+ return parseFold(window.localStorage.getItem(STORE_KEY))
81
+ } catch (error) {
82
+ return []
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 写入展开状态(配额满/隐私模式失败静默)。
88
+ * @author ddj 2026年09月02号
89
+ * @param expanded 已展开键集合
90
+ */
91
+ export function foldSave(expanded: string[]): void {
92
+ try {
93
+ window.localStorage.setItem(STORE_KEY, serializeFold(expanded))
94
+ } catch (error) { /* 配额满/隐私模式忽略 */ }
95
+ }
@@ -10,18 +10,36 @@
10
10
  .vsm-primary:hover{background:var(--dsw-alias-button-primary-hover,#5a46e6)}
11
11
  .vsm-primary:disabled{opacity:.55;cursor:default}
12
12
 
13
- /* 子 Tab:我的 MCP / MCP 市场 */
13
+ /* MCP 管理下的二级子页签:我的 MCP / 项目 MCP / MCP 市场 */
14
+ .vsm-mcp-subhead{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-top:-4px}
15
+ .vsm-mcp-subtabs{display:flex;gap:18px;border-bottom:1px solid var(--dsw-alias-border-l1,#2e2e2e);flex:1}
16
+ .vsm-mcp-subtabs button{border:0;background:none;color:var(--dsw-alias-label-tertiary,#9aa5b1);padding:7px 2px 9px;cursor:pointer;font-size:13px;position:relative}
17
+ .vsm-mcp-subtabs button:hover{color:var(--dsw-alias-label-secondary,#c8c8c8)}
18
+ .vsm-mcp-subtabs button.active{color:var(--dsw-alias-label-primary,#fff);font-weight:500}
19
+ .vsm-mcp-subtabs button.active::after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--dsw-alias-brand-primary,#6d55ff);border-radius:2px}
20
+
21
+ /* 顶层 Tab:通用 / 快捷键 / MCP 管理 / 语言服务器 / 性能优化 / 兼容性 */
14
22
  .vsm-mcp-tabs{display:flex;gap:24px;border-bottom:1px solid var(--dsw-alias-border-l2,#3a3a3a)}
15
23
  .vsm-mcp-tabs button{border:0;background:none;color:var(--dsw-alias-label-tertiary,#9aa5b1);padding:9px 2px 11px;cursor:pointer;font-size:14px;position:relative}
16
24
  .vsm-mcp-tabs button:hover{color:var(--dsw-alias-label-secondary,#c8c8c8)}
17
25
  .vsm-mcp-tabs button.active{color:var(--dsw-alias-label-primary,#fff);font-weight:500}
18
26
  .vsm-mcp-tabs button.active::after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--dsw-alias-brand-primary,#6d55ff);border-radius:2px}
19
27
 
28
+ /* 统一线框面板:圆角矩形边框圈住各设置分区,提升可读性。
29
+ 用法:<section class="vsm-panel"> + 可选 <h3 class="vsm-panel-title">。 */
30
+ .vsm-panel{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:12px;background:var(--dsw-alias-bg-layer-2,transparent);overflow:hidden}
31
+ .vsm-panel-pad{padding:14px 16px}
32
+ .vsm-panel-title{margin:0;padding:10px 16px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-secondary,#c8c8c8);background:var(--dsw-alias-bg-mask-1,#ffffff0a);border-bottom:1px solid var(--dsw-alias-border-l1,#333)}
33
+ .vsm-panel-body{padding:6px 16px}
34
+ .vsm-panel-body>*:first-child{margin-top:8px}
35
+ .vsm-panel-body>*:last-child{margin-bottom:10px}
36
+
20
37
  /* 通用设置 */
21
- .vsm-general-page{display:flex;flex-direction:column;gap:10px;max-width:720px}
38
+ .vsm-general-page{display:flex;flex-direction:column;gap:14px;max-width:720px}
22
39
  .vsm-general-page h2{margin:0;font-size:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}
23
- .vsm-general-page p{margin:0 0 8px;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:13px}
40
+ .vsm-general-page p{margin:0;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:13px}
24
41
  .vsm-general-row{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:14px 0;border-bottom:1px solid var(--dsw-alias-border-l1,#333);color:var(--dsw-alias-label-primary,#eee);font-size:14px}
42
+ .vsm-general-row:last-child{border-bottom:0}
25
43
  .vsm-general-row select{min-width:220px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:var(--dsw-alias-bg-layer-1,#2b2b2b);color:var(--dsw-alias-label-primary,#eee);font:inherit}
26
44
  .vsm-general-row small{color:var(--dsw-alias-label-tertiary,#9aa5b1)}
27
45
 
@@ -94,8 +112,7 @@
94
112
  .vsm-mcp-dialog-actions button{padding:8px 14px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:none;color:var(--dsw-alias-label-primary,#eee);cursor:pointer;font-size:13px}
95
113
  .vsm-mcp-dialog-actions button:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
96
114
 
97
- /* 兼容性小节 */
98
- .vsm-compat-h3{margin:16px 0 2px;font-size:14px;font-weight:600;color:var(--dsw-alias-label-secondary,#c8c8c8)}
115
+ /* 兼容性条目(面板标题改用 .vsm-panel-title) */
99
116
  .vsm-compat-item{display:flex;align-items:baseline;justify-content:space-between;gap:16px;padding:10px 0;border-bottom:1px solid var(--dsw-alias-border-l1,#333);font-size:13px;color:var(--dsw-alias-label-primary,#eee)}
100
117
  .vsm-compat-item.ok .vsm-compat-name::before{content:"● ";color:var(--dsw-alias-state-success-primary,#38bd75)}
101
118
  .vsm-compat-item.warn .vsm-compat-name::before{content:"● ";color:var(--dsw-alias-state-warn-primary,#e5b93f)}
@@ -139,6 +156,7 @@
139
156
 
140
157
  /* 会话性能(PerfSettings 子 Tab) */
141
158
  .vsm-perf-page{display:flex;flex-direction:column;gap:14px;max-width:920px}
159
+ .vsm-perf-page>.vsm-panel-body>*:first-child{margin-top:0}
142
160
  .vsm-perf-page h2{margin:0;font-size:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}
143
161
  .vsm-perf-page p{margin:0;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:13px;line-height:1.5}
144
162
  .vsm-perf-cards{display:flex;gap:12px;flex-wrap:wrap}
@@ -148,8 +166,19 @@
148
166
  .vsm-perf-card b.big{color:var(--dsw-alias-state-error-primary,#e05252)}
149
167
  .vsm-perf-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
150
168
  .vsm-perf-selcount{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);margin:0 4px}
169
+ /* 工作区列表分页器 */
170
+ .vsm-perf-pager{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-top:4px}
171
+ .vsm-perf-pager-text{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1)}
172
+ .vsm-perf-pager-actions{display:flex;gap:8px}
173
+ .vsm-perf-ws-list{display:flex;flex-direction:column;gap:8px}
151
174
  .vsm-perf-ws{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;overflow:hidden}
152
- .vsm-perf-ws-head{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:10px 14px;background:var(--dsw-alias-bg-layer-1,#262626);border-bottom:1px solid var(--dsw-alias-border-l2,#3a3a3a)}
175
+ .vsm-perf-ws-head{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--dsw-alias-bg-layer-1,#262626);cursor:pointer;user-select:none}
176
+ .vsm-perf-ws-head:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
177
+ .vsm-perf-ws-head:focus-visible{outline:1px solid var(--dsw-alias-brand-primary,#6d55ff);outline-offset:-1px}
178
+ .vsm-perf-ws.expanded .vsm-perf-ws-head{border-bottom:1px solid var(--dsw-alias-border-l2,#3a3a3a)}
179
+ .vsm-perf-ws-caret{color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:12px;flex-shrink:0;transition:transform .15s;display:inline-block}
180
+ .vsm-perf-ws-caret.open{transform:rotate(90deg)}
181
+ .vsm-perf-ws .vsm-perf-ws-name{flex:1}
153
182
  .vsm-perf-ws-name{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6);font-family:var(--ds-font-family-code,ui-monospace,monospace);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
154
183
  .vsm-perf-ws-meta{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);flex-shrink:0}
155
184
  .vsm-perf-rows{display:flex;flex-direction:column}
@@ -163,15 +192,9 @@
163
192
  .vsm-perf-size.big{color:var(--dsw-alias-state-error-primary,#e05252);font-weight:600}
164
193
  .vsm-perf-mtime{width:44px;flex-shrink:0;color:var(--dsw-alias-label-tertiary,#9aa5b1)}
165
194
  .vsm-perf-active{width:44px;flex-shrink:0;font-size:11px;color:var(--dsw-alias-state-warn-primary,#e5b93f);text-align:right}
166
- .vsm-perf-moved{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-2,transparent)}
167
- .vsm-perf-moved h3{margin:0 0 8px;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6)}
168
- .vsm-perf-config{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-2,transparent)}
169
- .vsm-perf-config h3{margin:0 0 8px;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6)}
170
195
  .vsm-perf-config-state{display:flex;align-items:center;gap:12px;margin:10px 0}
171
196
  .vsm-perf-note{font-size:12px;color:var(--dsw-alias-state-warn-primary,#e5b93f)}
172
197
  .vsm-perf-block{margin:8px 0 0;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:8px;background:var(--dsw-alias-bg-base,#111);color:var(--dsw-alias-label-secondary,#c8c8c8);font-size:11px;line-height:1.5;font-family:var(--ds-font-family-code,ui-monospace,monospace);white-space:pre-wrap;overflow-x:auto;max-width:100%}
173
- .vsm-perf-guide{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-2,transparent)}
174
- .vsm-perf-guide h3{margin:0 0 8px;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6)}
175
198
  .vsm-perf-modal{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:var(--dsw-alias-bg-mask-1,#000000aa);z-index:1000;padding:20px}
176
199
  .vsm-perf-modal-card{width:min(560px,92vw);max-height:72vh;display:flex;flex-direction:column;gap:12px;padding:18px 20px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#1f1f1f);color:var(--dsw-alias-label-primary,#e6e6e6)}
177
200
  .vsm-perf-modal-card h3{margin:0;font-size:16px}
@@ -178,10 +178,13 @@ export function KeybindingsPanel() {
178
178
  unavailable && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, '设置服务暂不可用,当前使用默认键位。'),
179
179
  error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
180
180
  message && React.createElement('div', { className: 'vsm-kb-message' }, message),
181
- React.createElement('div', { className: 'vsm-kb-list' }, rows),
182
- React.createElement('div', { className: 'vsm-kb-actionsbar' },
183
- React.createElement('span', { className: 'vsm-kb-dirty' }, dirty ? '有未保存的修改' : ''),
184
- React.createElement('button', { className: 'vsm-primary vsm-small', disabled: unavailable || notReady || busy || snapshot?.writable === false || !dirty, onClick: save }, busy ? '保存中…' : '保存'),
185
- React.createElement('button', { className: 'vsm-small', disabled: unavailable || notReady || busy || snapshot?.writable === false, onClick: resetAll }, '恢复默认')),
181
+ React.createElement('section', { className: 'vsm-panel' },
182
+ React.createElement('h3', { className: 'vsm-panel-title' }, '编辑器键位'),
183
+ React.createElement('div', { className: 'vsm-panel-body' }, React.createElement('div', { className: 'vsm-kb-list' }, rows)),
184
+ React.createElement('div', { className: 'vsm-panel-body vsm-kb-actionsbar' },
185
+ React.createElement('span', { className: 'vsm-kb-dirty' }, dirty ? '有未保存的修改' : ''),
186
+ React.createElement('button', { className: 'vsm-primary vsm-small', disabled: unavailable || notReady || busy || snapshot?.writable === false || !dirty, onClick: save }, busy ? '保存中…' : '保存'),
187
+ React.createElement('button', { className: 'vsm-small', disabled: unavailable || notReady || busy || snapshot?.writable === false, onClick: resetAll }, '恢复默认')),
188
+ ),
186
189
  )
187
190
  }
@@ -226,23 +226,29 @@ export function LspSettings() {
226
226
  React.createElement('button', { className: 'vsm-primary', onClick: refresh }, '↻ 刷新'))
227
227
  const banner = error ? React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error) : null
228
228
 
229
+ /** 线框面板容器:标题 + 内容体。 */
230
+ const panel = (title, children) => React.createElement('section', { className: 'vsm-panel' },
231
+ React.createElement('h3', { className: 'vsm-panel-title' }, title),
232
+ React.createElement('div', { className: 'vsm-panel-body' }, children),
233
+ )
234
+
229
235
  let body = null
230
236
  if (tab === 'servers') {
231
- body = React.createElement('div', null,
237
+ body = panel('语言服务器', React.createElement(React.Fragment, null,
232
238
  LANGUAGES.map((lang) => React.createElement(LangCard, {
233
239
  key: lang.id, lang,
234
240
  config: config[lang.id] ?? {},
235
241
  status: servers.find((s) => s.languageId === lang.id),
236
242
  busy, onToggle: toggleLang, onSave: saveLang,
237
243
  })),
238
- React.createElement('div', { className: 'vsm-lsp-hint' }, '提示:打开 Lua/C# 文件即自动(惰性)启动对应语言服务器;未配置时编辑器功能不受影响,大纲回退内置解析。'))
244
+ React.createElement('div', { className: 'vsm-lsp-hint' }, '提示:打开 Lua/C# 文件即自动(惰性)启动对应语言服务器;未配置时编辑器功能不受影响,大纲回退内置解析。')))
239
245
  } else if (tab === 'installed') {
240
- body = React.createElement('div', null,
246
+ body = panel('已安装扩展', React.createElement(React.Fragment, null,
241
247
  React.createElement('div', { className: 'vsm-lsp-hint' }, '已安装的语言服务器扩展(存于 ~/.dsh/dsh-vscode-mode/extensions/)。语言服务器被自动发现为「扩展」源。'),
242
248
  installed.length ? installed.map((ext) => React.createElement(InstalledCard, { key: ext.id, ext, busy, onUninstall: uninstallExt, onUpdate: updateExt }))
243
- : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有安装扩展(去「市场」或指定本地 .vsix)'))
249
+ : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有安装扩展(去「市场」或指定本地 .vsix)')))
244
250
  } else if (tab === 'market') {
245
- body = React.createElement('div', null,
251
+ body = panel('扩展市场', React.createElement(React.Fragment, null,
246
252
  React.createElement('div', { className: 'vsm-lsp-form' },
247
253
  React.createElement('label', null, React.createElement('span', null, '搜索 Open VSX 市场'), React.createElement('input', { value: query, placeholder: '如 sumneko.lua / lua language server', onChange: (e) => setQuery(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter') searchMarket() } })),
248
254
  React.createElement('button', { className: 'vsm-primary', disabled: busy === '__market', onClick: searchMarket }, busy === '__market' ? '搜索中…' : '搜索')),
@@ -250,9 +256,9 @@ export function LspSettings() {
250
256
  React.createElement('label', null, React.createElement('span', null, '安装本地 .vsix 文件(服务器绝对路径)'), React.createElement('input', { value: vsixPath, placeholder: '如 C:/Downloads/sumneko.lua-3.19.1.vsix', onChange: (e) => setVsixPath(e.target.value) })),
251
257
  React.createElement('button', { className: 'vsm-primary', disabled: busy === '__vsix', onClick: installLocal }, busy === '__vsix' ? '安装中…' : '安装本地 .vsix')),
252
258
  React.createElement('div', { className: 'vsm-lsp-hint' }, '市场安装会下载并解包 VSIX 到扩展目录;含语言服务器(如 sumneko.lua)的扩展会被自动发现。'),
253
- market.map((item) => React.createElement(MarketCard, { key: item.id, item, installed, busy, onInstall: installMarket })))
259
+ market.map((item) => React.createElement(MarketCard, { key: item.id, item, installed, busy, onInstall: installMarket }))))
254
260
  } else {
255
- body = React.createElement('div', null,
261
+ body = panel('可用更新', React.createElement(React.Fragment, null,
256
262
  React.createElement('div', { className: 'vsm-lsp-hint' }, '已装扩展与 Open VSX 最新版的差异。'),
257
263
  updates.length ? updates.map((u) => React.createElement('article', { key: u.id, className: 'vsm-mcp-card' },
258
264
  React.createElement('div', { className: 'vsm-mcp-card-head' },
@@ -262,7 +268,7 @@ export function LspSettings() {
262
268
  React.createElement('span', { className: 'vsm-ext-ver' }, 'v' + u.current + ' → v' + u.latest)),
263
269
  React.createElement('div', { className: 'vsm-mcp-actions' },
264
270
  React.createElement('button', { className: 'vsm-primary vsm-small', disabled: busy === u.id, onClick: () => updateExt(u) }, busy === u.id ? '更新中…' : '更新')))))
265
- : React.createElement('div', { className: 'vsm-mcp-empty' }, '全部已是最新'))
271
+ : React.createElement('div', { className: 'vsm-mcp-empty' }, '全部已是最新')))
266
272
  }
267
273
  return React.createElement('div', null, header, nav, banner, body)
268
274
  }
@@ -1,9 +1,8 @@
1
1
  // @ts-nocheck
2
2
  /**
3
- * dsh-vscode-mode client — VSCodeMode 设置区:通用 / 快捷键 / MCP 管理。
4
- * 子 Tab:通用(文件打开工具/开发形态)/ 快捷键(编辑器键位配置,可重置)/
5
- * 我的 MCP(profile 全局)/ 项目 MCP(各项目 .mcp.json)/ MCP 市场(占位)/ 兼容性。
6
- * 作者 ddj 2026年08月22号 / 2026年08月26号
3
+ * dsh-vscode-mode client — VSCodeMode 设置区:通用 / 快捷键 / MCP 管理 / 语言服务器 / 性能优化 / 兼容性。
4
+ * MCP 管理 Tab 下含三个子页签:我的 MCP(profile 全局)/ 项目 MCP(各项目 .mcp.json)/ MCP 市场(占位)。
5
+ * 作者 ddj 2026年08月22号 / 2026年08月26号 / 2026年08月27号
7
6
  */
8
7
  import React from 'react'
9
8
  import { rpc } from '../rpc.js'
@@ -178,8 +177,13 @@ function GeneralSettings({ registry }) {
178
177
  React.createElement('p', null, '配置 DSH 对话中文件链接的打开工具。工具由当前运行时自动扫描。'),
179
178
  unavailable && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, '设置服务暂不可用,当前使用自动选择。'),
180
179
  error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
181
- React.createElement('label', { className: 'vsm-general-row' }, React.createElement('span', null, '文件链接使用工具'), React.createElement('select', { value: currentOption, disabled: loading || unavailable || notReady || busy || snapshot?.writable === false, onChange: (event) => save(event.target.value) }, options.map((item) => React.createElement('option', { key: item.id, value: item.id }, item.label))), loading && React.createElement('small', null, '读取中…')),
182
- devFormRow,
180
+ React.createElement('section', { className: 'vsm-panel' },
181
+ React.createElement('h3', { className: 'vsm-panel-title' }, '文件与安装'),
182
+ React.createElement('div', { className: 'vsm-panel-body' },
183
+ React.createElement('label', { className: 'vsm-general-row' }, React.createElement('span', null, '文件链接使用工具'), React.createElement('select', { value: currentOption, disabled: loading || unavailable || notReady || busy || snapshot?.writable === false, onChange: (event) => save(event.target.value) }, options.map((item) => React.createElement('option', { key: item.id, value: item.id }, item.label))), loading && React.createElement('small', null, '读取中…')),
184
+ devFormRow,
185
+ ),
186
+ ),
183
187
  )
184
188
  }
185
189
 
@@ -200,24 +204,89 @@ function CompatSection({ getSummary }) {
200
204
  React.createElement('span', { className: 'vsm-compat-name' }, item.name),
201
205
  React.createElement('span', { className: 'vsm-compat-note' }, item.note ?? (item.active ? '正常' : '未生效')),
202
206
  ))
207
+ /** 兼容性分组面板:标题 + 线框内容区。 */
208
+ const group = (title, children) => React.createElement('section', { className: 'vsm-panel' },
209
+ React.createElement('h3', { className: 'vsm-panel-title' }, title),
210
+ React.createElement('div', { className: 'vsm-panel-body' }, children),
211
+ )
203
212
  const warnings = report?.warnings ?? []
204
213
  const devForm = report?.devForm
205
214
  return React.createElement('section', { className: 'vsm-general-page' },
206
215
  React.createElement('h2', null, '兼容性'),
207
216
  React.createElement('p', null, '检测与其他插件 / DSH 版本的适配状态与已知问题。' + (report?.pluginVersion ? '(插件版本 ' + report.pluginVersion + ')' : '')),
208
217
  error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
209
- devForm?.enabled && React.createElement(React.Fragment, null,
210
- React.createElement('h3', { className: 'vsm-compat-h3' }, '开发形态'),
211
- React.createElement('div', { className: 'vsm-compat-item ok' }, React.createElement('span', { className: 'vsm-compat-name' }, '开发形态开启(工作区链接安装)'), React.createElement('span', { className: 'vsm-compat-note' }, devForm.path)),
218
+ devForm?.enabled && group('开发形态', React.createElement('div', { className: 'vsm-compat-item ok' }, React.createElement('span', { className: 'vsm-compat-name' }, '开发形态开启(工作区链接安装)'), React.createElement('span', { className: 'vsm-compat-note' }, devForm.path))),
219
+ group('外部插件与服务', renderItems(report?.external)),
220
+ group('客户端适配', renderItems(getSummary?.() ?? [])),
221
+ group('护栏', renderItems(report?.guards)),
222
+ group('警告', warnings.length ? warnings.map((w, index) => React.createElement('div', { key: index, className: 'vsm-mcp-error vsm-mcp-banner' }, w)) : React.createElement('div', { className: 'vsm-compat-item ok' }, React.createElement('span', { className: 'vsm-compat-name' }, '未检测到兼容性问题'), React.createElement('span', { className: 'vsm-compat-note' }, ''))),
223
+ )
224
+ }
225
+
226
+ /** MCP 管理子页签定义(我的 / 项目 / 市场)。 */
227
+ const MCP_TABS = [
228
+ { id: 'mine', label: '我的 MCP' },
229
+ { id: 'projects', label: '项目 MCP' },
230
+ { id: 'market', label: 'MCP 市场' },
231
+ ]
232
+
233
+ /**
234
+ * MCP 管理面板:顶层「MCP 管理」Tab 下的三个子页签容器。
235
+ * 作者 ddj 2026年08月27号
236
+ * @param {object[]} servers 全局 MCP 服务列表
237
+ * @param {object[]} projects 项目及其项目级 MCP 列表
238
+ * @param {string} busy 正在执行的操作标识
239
+ * @param {object} draft 新增表单草稿
240
+ * @param {Function} edit 草稿字段变更回调
241
+ * @param {Function} resetDraft 清空草稿回调
242
+ */
243
+ function McpManagePanel({ servers, projects, busy, draft, edit, resetDraft, refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject, saveGlobal, saveProject }) {
244
+ const [mcpTab, setMcpTab] = React.useState('mine')
245
+ const [showForm, setShowForm] = React.useState(false)
246
+ const [projectForm, setProjectForm] = React.useState(null)
247
+ const [selectedPath, setSelectedPath] = React.useState('')
248
+
249
+ React.useEffect(() => {
250
+ if (!projects.length) {
251
+ setSelectedPath('')
252
+ return
253
+ }
254
+ if (!projects.some((project) => project.workspacePath === selectedPath)) setSelectedPath(projects[0].workspacePath)
255
+ }, [projects, selectedPath])
256
+
257
+ const closeGlobalForm = () => setShowForm(false)
258
+ const closeProjectForm = () => setProjectForm(null)
259
+ const addProject = (project) => {
260
+ resetDraft?.()
261
+ setProjectForm({ workspacePath: project.workspacePath, title: project.title })
262
+ }
263
+
264
+ const selectedProject = projects.find((project) => project.workspacePath === selectedPath)
265
+ const mineBody = servers.length
266
+ ? servers.map((s) => React.createElement(ServerCard, { key: s.id, server: s, onRefresh: refreshGlobal, onToggle: toggleGlobal, onRemove: removeGlobal }))
267
+ : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有配置全局 MCP')
268
+ const projectBody = projects.length ? React.createElement(React.Fragment, null,
269
+ React.createElement(ProjectPicker, { projects, value: selectedPath, onChange: setSelectedPath }),
270
+ selectedProject ? React.createElement(ProjectGroup, { project: selectedProject, busy, onAdd: addProject, onRefresh: refreshProject, onToggle: toggleProject, onRemove: removeProject }) : React.createElement('div', { className: 'vsm-mcp-empty' }, '请选择项目'),
271
+ ) : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有项目')
272
+
273
+ let body
274
+ if (mcpTab === 'projects') body = projectBody
275
+ else if (mcpTab === 'market') body = React.createElement('div', { className: 'vsm-mcp-empty' }, 'MCP 市场暂未接入')
276
+ else body = mineBody
277
+
278
+ const form = showForm
279
+ ? React.createElement(McpForm, { title: '添加全局 MCP', draft, busy, edit, save: () => saveGlobal(closeGlobalForm), close: closeGlobalForm })
280
+ : projectForm
281
+ ? React.createElement(McpForm, { title: '添加项目 MCP · ' + projectForm.title, draft, busy, edit, save: () => saveProject(projectForm.workspacePath, closeProjectForm), close: closeProjectForm })
282
+ : null
283
+ return React.createElement(React.Fragment, null,
284
+ React.createElement('div', { className: 'vsm-mcp-subhead' },
285
+ React.createElement('nav', { className: 'vsm-mcp-subtabs' }, MCP_TABS.map((item) => React.createElement('button', { key: item.id, className: mcpTab === item.id ? 'active' : '', onClick: () => setMcpTab(item.id) }, item.label))),
286
+ React.createElement('button', { className: 'vsm-primary vsm-small', onClick: () => { resetDraft?.(); setShowForm(true) } }, '+ 添加全局 MCP'),
212
287
  ),
213
- React.createElement('h3', { className: 'vsm-compat-h3' }, '外部插件与服务'),
214
- renderItems(report?.external),
215
- React.createElement('h3', { className: 'vsm-compat-h3' }, '客户端适配'),
216
- renderItems(getSummary?.() ?? []),
217
- React.createElement('h3', { className: 'vsm-compat-h3' }, '护栏'),
218
- renderItems(report?.guards),
219
- React.createElement('h3', { className: 'vsm-compat-h3' }, '警告'),
220
- warnings.length ? warnings.map((w, index) => React.createElement('div', { key: index, className: 'vsm-mcp-error vsm-mcp-banner' }, w)) : React.createElement('div', { className: 'vsm-compat-item ok' }, React.createElement('span', { className: 'vsm-compat-name' }, '未检测到兼容性问题'), React.createElement('span', { className: 'vsm-compat-note' }, '')),
288
+ body,
289
+ form,
221
290
  )
222
291
  }
223
292
 
@@ -232,17 +301,6 @@ export function McpSettings(props) {
232
301
  const [error, setError] = React.useState('')
233
302
  const [tab, setTab] = React.useState('general')
234
303
  const [draft, setDraft] = React.useState(EMPTY)
235
- const [showForm, setShowForm] = React.useState(false)
236
- const [projectForm, setProjectForm] = React.useState(null)
237
- const [selectedPath, setSelectedPath] = React.useState('')
238
-
239
- React.useEffect(() => {
240
- if (!projects.length) {
241
- setSelectedPath('')
242
- return
243
- }
244
- if (!projects.some((project) => project.workspacePath === selectedPath)) setSelectedPath(projects[0].workspacePath)
245
- }, [projects, selectedPath])
246
304
 
247
305
  const loadAll = React.useCallback(() => {
248
306
  setLoading(true)
@@ -287,52 +345,55 @@ export function McpSettings(props) {
287
345
  const toggleProject = (p, s) => projectAction(p.workspacePath, s.serverName, 'mcp.projectToggle', { workspacePath: p.workspacePath, serverName: s.serverName, enabled: !s.enabled }, replaceProject(p.workspacePath))
288
346
  const removeProject = (p, s) => window.confirm('确认删除此项目的 MCP「' + s.serverName + '」?') && projectAction(p.workspacePath, s.serverName, 'mcp.projectRemove', { workspacePath: p.workspacePath, serverName: s.serverName }, replaceProject(p.workspacePath))
289
347
 
290
- const saveGlobal = () => {
348
+ /**
349
+ * 保存全局 MCP:成功后由回调关闭子面板。
350
+ * 作者 ddj 2026年08月27号
351
+ * @param {Function} close 关闭新增表单回调
352
+ */
353
+ const saveGlobal = (close) => {
291
354
  setBusy('save')
292
355
  setError('')
293
356
  rpc('mcp.save', { config: configOf(draft) }).then(finish).then((result) => {
294
357
  setServers((old) => old.some((s) => s.id === result.server.id) ? old.map((s) => s.id === result.server.id ? result.server : s) : old.concat(result.server))
295
358
  setDraft(EMPTY)
296
- setShowForm(false)
359
+ close()
297
360
  }).catch((e) => setError(String(e))).finally(() => setBusy(''))
298
361
  }
299
362
 
300
- const saveProject = () => {
301
- if (!projectForm) return
363
+ /**
364
+ * 保存项目 MCP:成功后由回调关闭子面板。
365
+ * 作者 ddj 2026年08月27号
366
+ * @param {string} workspacePath 目标项目路径
367
+ * @param {Function} close 关闭新增表单回调
368
+ */
369
+ const saveProject = (workspacePath, close) => {
302
370
  setBusy('project-save')
303
371
  setError('')
304
372
  const config = configOf(draft)
305
- rpc('mcp.projectSave', { workspacePath: projectForm.workspacePath, serverName: config.serverName, config }).then(finish).then((result) => {
306
- setProjects((old) => old.map((p) => p.workspacePath === projectForm.workspacePath ? result.project : p))
373
+ rpc('mcp.projectSave', { workspacePath, serverName: config.serverName, config }).then(finish).then((result) => {
374
+ setProjects((old) => old.map((p) => p.workspacePath === workspacePath ? result.project : p))
307
375
  setDraft(EMPTY)
308
- setProjectForm(null)
376
+ close()
309
377
  }).catch((e) => setError(String(e))).finally(() => setBusy(''))
310
378
  }
311
379
 
312
380
  const edit = (key, value) => setDraft((old) => ({ ...old, [key]: value }))
313
- const marketBody = React.createElement('div', { className: 'vsm-mcp-empty' }, 'MCP 市场暂未接入')
314
- const selectedProject = projects.find((project) => project.workspacePath === selectedPath)
315
- const projectBody = projects.length ? React.createElement(React.Fragment, null,
316
- React.createElement(ProjectPicker, { projects, value: selectedPath, onChange: setSelectedPath }),
317
- selectedProject ? React.createElement(ProjectGroup, { project: selectedProject, busy, onAdd: (project) => { setDraft(EMPTY); setProjectForm({ workspacePath: project.workspacePath, title: project.title }) }, onRefresh: refreshProject, onToggle: toggleProject, onRemove: removeProject }) : React.createElement('div', { className: 'vsm-mcp-empty' }, '请选择项目'),
318
- ) : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有项目')
381
+ const resetDraft = () => setDraft(EMPTY)
319
382
  let body
320
383
  if (loading) body = React.createElement('div', { className: 'vsm-mcp-empty' }, '正在读取 MCP 服务…')
321
384
  else if (tab === 'general') body = React.createElement(GeneralSettings, { registry: openerRegistry })
322
385
  else if (tab === 'keys') body = React.createElement(KeybindingsPanel, null)
323
- else if (tab === 'mine') body = React.createElement('div', null, servers.length ? servers.map((s) => React.createElement(ServerCard, { key: s.id, server: s, onRefresh: refreshGlobal, onToggle: toggleGlobal, onRemove: removeGlobal })) : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有配置全局 MCP'))
324
- else if (tab === 'projects') body = projectBody
386
+ else if (tab === 'mcp') body = React.createElement(McpManagePanel, { servers, projects, busy, draft, edit, resetDraft,
387
+ saveGlobal: (close) => saveGlobal(close), saveProject: (workspacePath, close) => saveProject(workspacePath, close),
388
+ refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject })
325
389
  else if (tab === 'compat') body = React.createElement(CompatSection, { getSummary: compatSummary })
326
390
  else if (tab === 'lsp') body = React.createElement(LspSettings, null)
327
- else if (tab === 'perf') body = React.createElement(PerfSettings, null)
328
- else body = marketBody
329
- const form = showForm ? React.createElement(McpForm, { title: '添加全局 MCP', draft, busy, edit, save: saveGlobal, close: () => setShowForm(false) }) : projectForm ? React.createElement(McpForm, { title: '添加项目 MCP · ' + projectForm.title, draft, busy, edit, save: saveProject, close: () => setProjectForm(null) }) : null
391
+ else body = React.createElement(PerfSettings, null)
330
392
  return React.createElement('section', { className: 'vsm-mcp-page' },
331
- React.createElement('header', { className: 'vsm-mcp-header' }, React.createElement('div', null, React.createElement('h2', null, 'VSCodeMode'), React.createElement('p', null, '管理当前 profile 与各项目的 Model Context Protocol 服务。')), React.createElement('button', { className: 'vsm-primary', onClick: () => { setDraft(EMPTY); setShowForm(true) } }, '+ 添加全局 MCP')),
332
- React.createElement('nav', { className: 'vsm-mcp-tabs' }, React.createElement('button', { className: tab === 'general' ? 'active' : '', onClick: () => setTab('general') }, '通用'), React.createElement('button', { className: tab === 'keys' ? 'active' : '', onClick: () => setTab('keys') }, '快捷键'), React.createElement('button', { className: tab === 'mine' ? 'active' : '', onClick: () => setTab('mine') }, '我的 MCP'), React.createElement('button', { className: tab === 'projects' ? 'active' : '', onClick: () => setTab('projects') }, '项目 MCP'), React.createElement('button', { className: tab === 'lsp' ? 'active' : '', onClick: () => setTab('lsp') }, '语言服务器'), React.createElement('button', { className: tab === 'perf' ? 'active' : '', onClick: () => setTab('perf') }, '性能优化'), React.createElement('button', { className: tab === 'market' ? 'active' : '', onClick: () => setTab('market') }, 'MCP 市场'), React.createElement('button', { className: tab === 'compat' ? 'active' : '', onClick: () => setTab('compat') }, '兼容性')),
393
+ React.createElement('header', { className: 'vsm-mcp-header' }, React.createElement('div', null, React.createElement('h2', null, 'VSCodeMode'), React.createElement('p', null, '管理当前 profile 与各项目的 Model Context Protocol 服务。'))),
394
+ React.createElement('nav', { className: 'vsm-mcp-tabs' }, React.createElement('button', { className: tab === 'general' ? 'active' : '', onClick: () => setTab('general') }, '通用'), React.createElement('button', { className: tab === 'keys' ? 'active' : '', onClick: () => setTab('keys') }, '快捷键'), React.createElement('button', { className: tab === 'mcp' ? 'active' : '', onClick: () => setTab('mcp') }, 'MCP 管理'), React.createElement('button', { className: tab === 'lsp' ? 'active' : '', onClick: () => setTab('lsp') }, '语言服务器'), React.createElement('button', { className: tab === 'perf' ? 'active' : '', onClick: () => setTab('perf') }, '性能优化'), React.createElement('button', { className: tab === 'compat' ? 'active' : '', onClick: () => setTab('compat') }, '兼容性')),
333
395
  error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
334
396
  body,
335
- form,
336
397
  )
337
398
  }
338
399