dsh-vscode-mode 0.1.35 → 0.1.42

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.
@@ -9,6 +9,7 @@ import React from 'react'
9
9
  import { dbg, rpc } from '../rpc.js'
10
10
  import { emitRefresh } from '../events.js'
11
11
  import { langOf, loadMonaco } from '../monaco/loader.js'
12
+ import { applyTheme, registerThemes, themeNameOf } from '../monaco/theme.js'
12
13
  import { createDiffRenderer } from '../monaco/diffRender.js'
13
14
  import { ST, callIdAttr, noopHunk, summarize } from '../state/records.js'
14
15
  import { diffRegions } from '../state/regions.js'
@@ -244,7 +245,9 @@ export function EditorView(props) {
244
245
  setLoadStage((prev) => ({ progress: Math.max(84, prev.progress), message: '文件已读取,准备创建编辑器…' }))
245
246
  setStatus('已加载')
246
247
  } else {
247
- const message = res?.error ? String(res.error) : '读取失败'
248
+ const base = res?.error ? String(res.error) : '读取失败'
249
+ // 带出 host 解析后的真实路径:跳转失败时一眼看出是路径解析错还是目标不存在
250
+ const message = res?.resolvedPath ? base + ':' + String(res.resolvedPath) : base
248
251
  setLoadError(message)
249
252
  setError(message)
250
253
  setStatus('读取失败')
@@ -496,6 +499,22 @@ export function EditorView(props) {
496
499
  return () => { alive = false }
497
500
  }, [monaco, monacoErr])
498
501
 
502
+ // 分色主题:Monaco 就绪后注册(幂等)+ 应用;DSH 明暗切换时跟随重刷。
503
+ React.useEffect(() => {
504
+ if (!monaco) return
505
+ registerThemes(monaco)
506
+ applyTheme(monaco)
507
+ if (typeof window.matchMedia !== 'function') return
508
+ const query = window.matchMedia('(prefers-color-scheme: dark)')
509
+ const onChange = () => applyTheme(monaco)
510
+ if (typeof query.addEventListener === 'function') {
511
+ query.addEventListener('change', onChange)
512
+ return () => query.removeEventListener('change', onChange)
513
+ }
514
+ query.addListener(onChange)
515
+ return () => query.removeListener(onChange)
516
+ }, [monaco])
517
+
499
518
  monacoRef.current = monaco
500
519
 
501
520
  const getModel = (path, text) => {
@@ -587,7 +606,7 @@ export function EditorView(props) {
587
606
  const ed = m.editor.create(node, {
588
607
  value: '',
589
608
  language: 'plaintext',
590
- theme: 'vs',
609
+ theme: themeNameOf(),
591
610
  fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)',
592
611
  fontSize: 13,
593
612
  lineHeight: 20,
@@ -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
  }
@@ -10,7 +10,7 @@ import { rpc } from '../rpc.js'
10
10
  import '../styles/mcp.css'
11
11
 
12
12
  const LANGUAGES = [
13
- { id: 'lua', label: 'Lua(LuaLS)', hint: 'lua-language-server,EmmyLua 注解原生支持' },
13
+ { id: 'lua', label: 'Lua(EmmyLua / LuaLS)', hint: '优先使用已安装 EmmyLua;未安装时回退 LuaLS 或 PATH 中的 lua-language-server' },
14
14
  { id: 'csharp', label: 'C#(Roslyn / OmniSharp)', hint: '需 dotnet + ms-dotnettools.csharp 的 .roslyn 服务器,或手动指定' },
15
15
  ]
16
16
 
@@ -25,8 +25,8 @@ const TABS = [
25
25
  { id: 'updates', label: '更新' },
26
26
  ]
27
27
 
28
- /** 语言卡片:状态 + 启用开关 + 命令/路径配置。 */
29
- function LangCard({ lang, config, status, busy, onToggle, onSave }) {
28
+ /** 语言卡片:状态 + 启用开关 + 命令/路径配置 + 重新检测。 */
29
+ function LangCard({ lang, config, status, busy, onToggle, onSave, onRedetect }) {
30
30
  const [pathDraft, setPathDraft] = React.useState(config?.path ?? '')
31
31
  const [commandDraft, setCommandDraft] = React.useState(config?.command ?? '')
32
32
  React.useEffect(() => { setPathDraft(config?.path ?? ''); setCommandDraft(config?.command ?? '') }, [config?.path, config?.command])
@@ -40,12 +40,15 @@ function LangCard({ lang, config, status, busy, onToggle, onSave }) {
40
40
  React.createElement('button', { className: 'vsm-switch ' + (config?.enabled !== false ? 'on' : ''), onClick: () => onToggle(lang.id, config?.enabled !== false ? false : true), 'aria-label': '启用/禁用' }, config?.enabled !== false ? '●' : '○'))),
41
41
  React.createElement('div', { className: 'vsm-mcp-meta' },
42
42
  PHASE_LABEL[phase] ?? phase, ' · ', SOURCE_LABEL[status?.source] ?? status?.source,
43
+ status?.providerName ? ' · ' + status.providerName : '',
44
+ status?.version ? ' · v' + status.version : '',
43
45
  (phase === 'ready' && status?.root) ? ' · ' + String(status.root).split(/[\\/]/).pop() : ''),
44
46
  status?.reason ? React.createElement('div', { className: 'vsm-mcp-error' }, status.reason) : null,
45
47
  React.createElement('div', { className: 'vsm-lsp-form' },
46
48
  React.createElement('label', null, React.createElement('span', null, '可执行文件路径(绝对路径,优先)'), React.createElement('input', { value: pathDraft, placeholder: '如 C:/.../lua-language-server.exe', onChange: (e) => setPathDraft(e.target.value) })),
47
49
  React.createElement('label', null, React.createElement('span', null, '命令名(PATH 内查找)'), React.createElement('input', { value: commandDraft, placeholder: '如 lua-language-server', onChange: (e) => setCommandDraft(e.target.value) })),
48
- React.createElement('button', { className: 'vsm-primary', disabled: busy === lang.id, onClick: () => onSave(lang.id, pathDraft, commandDraft) }, busy === lang.id ? '保存中…' : '保存配置')),
50
+ React.createElement('button', { className: 'vsm-primary', disabled: busy === lang.id, onClick: () => onSave(lang.id, pathDraft, commandDraft) }, busy === lang.id ? '保存中…' : '保存配置'),
51
+ React.createElement('button', { className: 'vsm-primary vsm-small', disabled: busy === lang.id, onClick: () => onRedetect(lang.id), title: '清缓存并重新选择语言服务器(EmmyLua / LuaLS)' }, busy === lang.id ? '检测中…' : '重新检测')),
49
52
  React.createElement('div', { className: 'vsm-lsp-hint' }, lang.hint))
50
53
  }
51
54
 
@@ -129,6 +132,18 @@ export function LspSettings() {
129
132
  if (next === 'servers') void refreshServers()
130
133
  }
131
134
 
135
+ /** 合并 redetect/configUpdate 返回的服务器状态:只替换该语言条目。 */
136
+ const mergeServers = (languageId, list) => {
137
+ setServers((prev) => Array.isArray(list)
138
+ ? [...prev.filter((s) => s.languageId !== languageId), ...list]
139
+ : prev)
140
+ }
141
+
142
+ /** 重检测后通知编辑器:与 host 重新同步已打开的模型(触发重新 acquire)。 */
143
+ const notifyResync = (languageId) => {
144
+ window.dispatchEvent(new CustomEvent('edrv:lsp-redetect', { detail: { languageId } }))
145
+ }
146
+
132
147
  const saveLang = (languageId, path, command) => {
133
148
  setBusy(languageId)
134
149
  setError('')
@@ -136,6 +151,8 @@ export function LspSettings() {
136
151
  .then((res) => {
137
152
  if (!res?.ok) { setError(res?.error ?? '保存失败'); return }
138
153
  setConfig(res.config ?? {})
154
+ mergeServers(languageId, res.servers)
155
+ notifyResync(languageId) // 保存后自动重新检测 + 同步模型
139
156
  })
140
157
  .catch((e) => setError(String(e)))
141
158
  .finally(() => setBusy(''))
@@ -145,8 +162,23 @@ export function LspSettings() {
145
162
  setBusy(languageId)
146
163
  rpc('edrv.lsp.configUpdate', { languageId, enabled })
147
164
  .then((res) => {
148
- if (res?.ok) { setConfig(res.config ?? {}); void refreshServers() }
149
- else setError(res?.error ?? '切换失败')
165
+ if (!res?.ok) { setError(res?.error ?? '切换失败'); return }
166
+ setConfig(res.config ?? {})
167
+ mergeServers(languageId, res.servers)
168
+ notifyResync(languageId) // 切换启用状态后自动重新检测 + 同步模型
169
+ })
170
+ .catch((e) => setError(String(e)))
171
+ .finally(() => setBusy(''))
172
+ }
173
+
174
+ const redetectLang = (languageId) => {
175
+ setBusy(languageId)
176
+ setError('')
177
+ rpc('edrv.lsp.redetect', { languageId })
178
+ .then((res) => {
179
+ if (!res?.ok) { setError(res?.error ?? '重新检测失败'); return }
180
+ mergeServers(languageId, res.servers)
181
+ notifyResync(languageId)
150
182
  })
151
183
  .catch((e) => setError(String(e)))
152
184
  .finally(() => setBusy(''))
@@ -226,23 +258,29 @@ export function LspSettings() {
226
258
  React.createElement('button', { className: 'vsm-primary', onClick: refresh }, '↻ 刷新'))
227
259
  const banner = error ? React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error) : null
228
260
 
261
+ /** 线框面板容器:标题 + 内容体。 */
262
+ const panel = (title, children) => React.createElement('section', { className: 'vsm-panel' },
263
+ React.createElement('h3', { className: 'vsm-panel-title' }, title),
264
+ React.createElement('div', { className: 'vsm-panel-body' }, children),
265
+ )
266
+
229
267
  let body = null
230
268
  if (tab === 'servers') {
231
- body = React.createElement('div', null,
269
+ body = panel('语言服务器', React.createElement(React.Fragment, null,
232
270
  LANGUAGES.map((lang) => React.createElement(LangCard, {
233
271
  key: lang.id, lang,
234
272
  config: config[lang.id] ?? {},
235
273
  status: servers.find((s) => s.languageId === lang.id),
236
- busy, onToggle: toggleLang, onSave: saveLang,
274
+ busy, onToggle: toggleLang, onSave: saveLang, onRedetect: redetectLang,
237
275
  })),
238
- React.createElement('div', { className: 'vsm-lsp-hint' }, '提示:打开 Lua/C# 文件即自动(惰性)启动对应语言服务器;未配置时编辑器功能不受影响,大纲回退内置解析。'))
276
+ React.createElement('div', { className: 'vsm-lsp-hint' }, '提示:打开 Lua/C# 文件即自动(惰性)启动对应语言服务器;未配置时编辑器功能不受影响,大纲回退内置解析。')))
239
277
  } else if (tab === 'installed') {
240
- body = React.createElement('div', null,
278
+ body = panel('已安装扩展', React.createElement(React.Fragment, null,
241
279
  React.createElement('div', { className: 'vsm-lsp-hint' }, '已安装的语言服务器扩展(存于 ~/.dsh/dsh-vscode-mode/extensions/)。语言服务器被自动发现为「扩展」源。'),
242
280
  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)'))
281
+ : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有安装扩展(去「市场」或指定本地 .vsix)')))
244
282
  } else if (tab === 'market') {
245
- body = React.createElement('div', null,
283
+ body = panel('扩展市场', React.createElement(React.Fragment, null,
246
284
  React.createElement('div', { className: 'vsm-lsp-form' },
247
285
  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
286
  React.createElement('button', { className: 'vsm-primary', disabled: busy === '__market', onClick: searchMarket }, busy === '__market' ? '搜索中…' : '搜索')),
@@ -250,9 +288,9 @@ export function LspSettings() {
250
288
  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
289
  React.createElement('button', { className: 'vsm-primary', disabled: busy === '__vsix', onClick: installLocal }, busy === '__vsix' ? '安装中…' : '安装本地 .vsix')),
252
290
  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 })))
291
+ market.map((item) => React.createElement(MarketCard, { key: item.id, item, installed, busy, onInstall: installMarket }))))
254
292
  } else {
255
- body = React.createElement('div', null,
293
+ body = panel('可用更新', React.createElement(React.Fragment, null,
256
294
  React.createElement('div', { className: 'vsm-lsp-hint' }, '已装扩展与 Open VSX 最新版的差异。'),
257
295
  updates.length ? updates.map((u) => React.createElement('article', { key: u.id, className: 'vsm-mcp-card' },
258
296
  React.createElement('div', { className: 'vsm-mcp-card-head' },
@@ -262,7 +300,7 @@ export function LspSettings() {
262
300
  React.createElement('span', { className: 'vsm-ext-ver' }, 'v' + u.current + ' → v' + u.latest)),
263
301
  React.createElement('div', { className: 'vsm-mcp-actions' },
264
302
  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' }, '全部已是最新'))
303
+ : React.createElement('div', { className: 'vsm-mcp-empty' }, '全部已是最新')))
266
304
  }
267
305
  return React.createElement('div', null, header, nav, banner, body)
268
306
  }
@@ -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