dsh-vscode-mode 0.1.32 → 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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/lib/client.js +1229 -124
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +1959 -52
  5. package/lib/index.js.map +1 -1
  6. package/package.json +2 -2
  7. package/src/client/events.ts +11 -0
  8. package/src/client/index.ts +15 -2
  9. package/src/client/monaco/lsp/index.ts +30 -0
  10. package/src/client/monaco/lsp/lspClient.ts +192 -0
  11. package/src/client/monaco/lsp/providers.ts +229 -0
  12. package/src/client/outline/OutlinePanel.tsx +1 -1
  13. package/src/client/outline/lspSource.ts +49 -0
  14. package/src/client/outline/parse.ts +31 -1
  15. package/src/client/outline/types.ts +2 -0
  16. package/src/client/paths.ts +2 -0
  17. package/src/client/state/workspaceFoldCache.ts +95 -0
  18. package/src/client/styles/editor.css +15 -0
  19. package/src/client/styles/mcp.css +50 -12
  20. package/src/client/ui/EditorView.ts +55 -1
  21. package/src/client/ui/KeybindingsPanel.ts +8 -5
  22. package/src/client/ui/LspSettings.ts +274 -0
  23. package/src/client/ui/McpSettings.ts +112 -49
  24. package/src/client/ui/PerfSettings.ts +145 -34
  25. package/src/fileOpenSettings.ts +1 -0
  26. package/src/index.ts +20 -2
  27. package/src/lsp/client.ts +205 -0
  28. package/src/lsp/config.ts +107 -0
  29. package/src/lsp/extmgr.ts +208 -0
  30. package/src/lsp/jsonrpc.ts +86 -0
  31. package/src/lsp/manager.ts +133 -0
  32. package/src/lsp/providers.ts +235 -0
  33. package/src/lsp/rpc.ts +334 -0
  34. package/src/lsp/server.ts +346 -0
  35. package/src/lsp/settings.ts +44 -0
  36. package/src/lsp/transport.ts +143 -0
  37. package/src/lsp/uri.ts +62 -0
  38. package/src/lsp/zip.ts +82 -0
  39. package/src/rpc.ts +11 -2
  40. package/src/shared/lsp.ts +138 -0
  41. package/src/shared/rpc.ts +33 -0
@@ -0,0 +1,274 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — 「语言服务器」设置子 Tab。
4
+ * 四视图:服务器(状态 + 每语言启用/命令/路径)|已安装(卸载/更新)|市场(Open VSX 搜索/安装 + 本地 vsix)|更新。
5
+ * 状态经 edrv.lsp.status,配置经 edrv.lsp.configGet/Update,扩展经 edrv.lsp.ext.*。
6
+ * 作者 ddj 2026-08-27
7
+ */
8
+ import React from 'react'
9
+ import { rpc } from '../rpc.js'
10
+ import '../styles/mcp.css'
11
+
12
+ const LANGUAGES = [
13
+ { id: 'lua', label: 'Lua(LuaLS)', hint: 'lua-language-server,EmmyLua 注解原生支持' },
14
+ { id: 'csharp', label: 'C#(Roslyn / OmniSharp)', hint: '需 dotnet + ms-dotnettools.csharp 的 .roslyn 服务器,或手动指定' },
15
+ ]
16
+
17
+ const PHASE_LABEL = {
18
+ idle: '未启动', starting: '启动中', ready: '就绪', indexing: '索引中', unavailable: '不可用', stopped: '已停止',
19
+ }
20
+ const SOURCE_LABEL = { extension: '扩展', discover: '自动发现', manual: '手动配置', none: '未配置' }
21
+ const TABS = [
22
+ { id: 'servers', label: '服务器' },
23
+ { id: 'installed', label: '已安装' },
24
+ { id: 'market', label: '市场' },
25
+ { id: 'updates', label: '更新' },
26
+ ]
27
+
28
+ /** 语言卡片:状态 + 启用开关 + 命令/路径配置。 */
29
+ function LangCard({ lang, config, status, busy, onToggle, onSave }) {
30
+ const [pathDraft, setPathDraft] = React.useState(config?.path ?? '')
31
+ const [commandDraft, setCommandDraft] = React.useState(config?.command ?? '')
32
+ React.useEffect(() => { setPathDraft(config?.path ?? ''); setCommandDraft(config?.command ?? '') }, [config?.path, config?.command])
33
+ const phase = status?.phase ?? 'idle'
34
+ return React.createElement('article', { className: 'vsm-mcp-card' },
35
+ React.createElement('div', { className: 'vsm-mcp-card-head' },
36
+ React.createElement('div', { className: 'vsm-mcp-title' },
37
+ React.createElement('span', { className: 'vsm-mcp-dot ' + (phase === 'ready' || phase === 'indexing' ? 'connected' : phase === 'unavailable' ? 'error' : 'disabled') }),
38
+ lang.label),
39
+ React.createElement('div', { className: 'vsm-mcp-actions' },
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
+ React.createElement('div', { className: 'vsm-mcp-meta' },
42
+ PHASE_LABEL[phase] ?? phase, ' · ', SOURCE_LABEL[status?.source] ?? status?.source,
43
+ (phase === 'ready' && status?.root) ? ' · ' + String(status.root).split(/[\\/]/).pop() : ''),
44
+ status?.reason ? React.createElement('div', { className: 'vsm-mcp-error' }, status.reason) : null,
45
+ React.createElement('div', { className: 'vsm-lsp-form' },
46
+ 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
+ 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 ? '保存中…' : '保存配置')),
49
+ React.createElement('div', { className: 'vsm-lsp-hint' }, lang.hint))
50
+ }
51
+
52
+ /** 已装扩展卡片:清单 + 卸载/更新。 */
53
+ function InstalledCard({ ext, busy, onUninstall, onUpdate }) {
54
+ return React.createElement('article', { className: 'vsm-mcp-card' },
55
+ React.createElement('div', { className: 'vsm-mcp-card-head' },
56
+ React.createElement('div', { className: 'vsm-mcp-title' },
57
+ React.createElement('span', { className: 'vsm-mcp-dot connected' }),
58
+ React.createElement('span', null, ext.displayName || ext.name),
59
+ React.createElement('span', { className: 'vsm-ext-ver' }, ext.id + ' · v' + ext.version)),
60
+ React.createElement('div', { className: 'vsm-mcp-actions' },
61
+ React.createElement('button', { className: 'vsm-primary vsm-small', disabled: busy === ext.id, onClick: () => onUpdate(ext) }, '更新'),
62
+ React.createElement('button', { className: 'vsm-danger vsm-small', disabled: busy === ext.id, onClick: () => onUninstall(ext) }, '卸载'))),
63
+ ext.description ? React.createElement('div', { className: 'vsm-lsp-hint' }, ext.description) : null)
64
+ }
65
+
66
+ /** 市场条目卡片:安装/已装态。 */
67
+ function MarketCard({ item, installed, busy, onInstall }) {
68
+ const has = installed.some((e) => e.id === item.id)
69
+ return React.createElement('article', { className: 'vsm-mcp-card' },
70
+ React.createElement('div', { className: 'vsm-mcp-card-head' },
71
+ React.createElement('div', { className: 'vsm-mcp-title' },
72
+ React.createElement('span', { className: 'vsm-mcp-dot ' + (has ? 'connected' : 'disabled') }),
73
+ React.createElement('span', null, item.displayName || item.name),
74
+ React.createElement('span', { className: 'vsm-ext-ver' }, item.id + ' · v' + item.version)),
75
+ React.createElement('div', { className: 'vsm-mcp-actions' },
76
+ React.createElement('button', { className: 'vsm-primary vsm-small', disabled: has || busy === item.id, onClick: () => onInstall(item) }, has ? '已安装' : '安装'))),
77
+ item.description ? React.createElement('div', { className: 'vsm-lsp-hint' }, item.description) : null)
78
+ }
79
+
80
+ /**
81
+ * 语言服务器设置页:服务器状态/配置 + VSIX 扩展(安装/更新/卸载三视图)。
82
+ * @author ddj 2026年08月27号
83
+ */
84
+ export function LspSettings() {
85
+ const [tab, setTab] = React.useState('servers')
86
+ const [config, setConfig] = React.useState({})
87
+ const [servers, setServers] = React.useState([])
88
+ const [installed, setInstalled] = React.useState([])
89
+ const [updates, setUpdates] = React.useState([])
90
+ const [market, setMarket] = React.useState([])
91
+ const [query, setQuery] = React.useState('')
92
+ const [vsixPath, setVsixPath] = React.useState('')
93
+ const [busy, setBusy] = React.useState('')
94
+ const [error, setError] = React.useState('')
95
+ const [loading, setLoading] = React.useState(true)
96
+
97
+ const refreshServers = React.useCallback(() => {
98
+ Promise.all([rpc('edrv.lsp.configGet', {}), rpc('edrv.lsp.status', {})])
99
+ .then(([cfg, st]) => {
100
+ if (cfg?.ok) setConfig(cfg.config ?? {})
101
+ if (st?.ok) setServers(st.servers ?? [])
102
+ setError('')
103
+ })
104
+ .catch((e) => setError(String(e)))
105
+ }, [])
106
+
107
+ const refreshExt = React.useCallback(() => {
108
+ Promise.all([rpc('edrv.lsp.ext.list', {}), rpc('edrv.lsp.ext.updates', {})])
109
+ .then(([l, u]) => {
110
+ if (l?.ok) setInstalled(l.extensions ?? [])
111
+ if (u?.ok) setUpdates(u.updates ?? [])
112
+ setError('')
113
+ })
114
+ .catch((e) => setError(String(e)))
115
+ }, [])
116
+
117
+ const refresh = React.useCallback(() => {
118
+ setLoading(true)
119
+ Promise.all([refreshServers(), refreshExt()]).finally(() => setLoading(false))
120
+ }, [refreshServers, refreshExt])
121
+
122
+ React.useEffect(() => { refresh() }, [refresh])
123
+
124
+ const switchTab = (next) => {
125
+ setTab(next)
126
+ setError('')
127
+ if (next === 'market' && !query) setQuery('lua language server')
128
+ if (next === 'installed' || next === 'updates') void refreshExt()
129
+ if (next === 'servers') void refreshServers()
130
+ }
131
+
132
+ const saveLang = (languageId, path, command) => {
133
+ setBusy(languageId)
134
+ setError('')
135
+ rpc('edrv.lsp.configUpdate', { languageId, path, command })
136
+ .then((res) => {
137
+ if (!res?.ok) { setError(res?.error ?? '保存失败'); return }
138
+ setConfig(res.config ?? {})
139
+ })
140
+ .catch((e) => setError(String(e)))
141
+ .finally(() => setBusy(''))
142
+ }
143
+
144
+ const toggleLang = (languageId, enabled) => {
145
+ setBusy(languageId)
146
+ rpc('edrv.lsp.configUpdate', { languageId, enabled })
147
+ .then((res) => {
148
+ if (res?.ok) { setConfig(res.config ?? {}); void refreshServers() }
149
+ else setError(res?.error ?? '切换失败')
150
+ })
151
+ .catch((e) => setError(String(e)))
152
+ .finally(() => setBusy(''))
153
+ }
154
+
155
+ const searchMarket = () => {
156
+ if (!query.trim()) return
157
+ setBusy('__market')
158
+ setError('')
159
+ rpc('edrv.lsp.ext.market', { query: query.trim(), size: 12 })
160
+ .then((res) => {
161
+ if (res?.ok) setMarket(res.extensions ?? [])
162
+ else setError(res?.error ?? '搜索失败')
163
+ })
164
+ .catch((e) => setError(String(e)))
165
+ .finally(() => setBusy(''))
166
+ }
167
+
168
+ const installMarket = (item) => {
169
+ setBusy(item.id)
170
+ setError('')
171
+ rpc('edrv.lsp.ext.install', { namespace: item.namespace, name: item.name })
172
+ .then((res) => {
173
+ if (!res?.ok) { setError(res?.error ?? '安装失败'); return }
174
+ void refreshExt()
175
+ })
176
+ .catch((e) => setError(String(e)))
177
+ .finally(() => setBusy(''))
178
+ }
179
+
180
+ const installLocal = () => {
181
+ if (!vsixPath.trim()) return
182
+ setBusy('__vsix')
183
+ setError('')
184
+ rpc('edrv.lsp.ext.install', { vsixPath: vsixPath.trim() })
185
+ .then((res) => {
186
+ if (!res?.ok) { setError(res?.error ?? '安装失败'); return }
187
+ setVsixPath('')
188
+ void refreshExt()
189
+ })
190
+ .catch((e) => setError(String(e)))
191
+ .finally(() => setBusy(''))
192
+ }
193
+
194
+ const uninstallExt = (ext) => {
195
+ if (!window.confirm('卸载扩展 ' + ext.id + '?')) return
196
+ setBusy(ext.id)
197
+ setError('')
198
+ rpc('edrv.lsp.ext.uninstall', { id: ext.id })
199
+ .then((res) => {
200
+ if (!res?.ok) setError(res?.error ?? '卸载失败')
201
+ void refreshExt()
202
+ })
203
+ .catch((e) => setError(String(e)))
204
+ .finally(() => setBusy(''))
205
+ }
206
+
207
+ const updateExt = (ext) => {
208
+ setBusy(ext.id)
209
+ setError('')
210
+ rpc('edrv.lsp.ext.update', { id: ext.id })
211
+ .then((res) => {
212
+ if (!res?.ok) setError(res?.error ?? '更新失败')
213
+ void refreshExt()
214
+ })
215
+ .catch((e) => setError(String(e)))
216
+ .finally(() => setBusy(''))
217
+ }
218
+
219
+ if (loading) return React.createElement('div', { className: 'vsm-mcp-empty' }, '正在读取语言服务器状态…')
220
+ const nav = React.createElement('nav', { className: 'vsm-mcp-tabs' },
221
+ TABS.map((t) => React.createElement('button', { key: t.id, className: tab === t.id ? 'active' : '', onClick: () => switchTab(t.id) }, t.label)))
222
+ const header = React.createElement('div', { className: 'vsm-mcp-header' },
223
+ React.createElement('div', null,
224
+ React.createElement('h2', null, '语言服务器(LSP)'),
225
+ React.createElement('p', null, '代码索引 / 引用查找 / 动态跳转;可安装 VSIX 语言服务器扩展(Open VSX 市场 / 本地 .vsix)。')),
226
+ React.createElement('button', { className: 'vsm-primary', onClick: refresh }, '↻ 刷新'))
227
+ const banner = error ? React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error) : null
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
+
235
+ let body = null
236
+ if (tab === 'servers') {
237
+ body = panel('语言服务器', React.createElement(React.Fragment, null,
238
+ LANGUAGES.map((lang) => React.createElement(LangCard, {
239
+ key: lang.id, lang,
240
+ config: config[lang.id] ?? {},
241
+ status: servers.find((s) => s.languageId === lang.id),
242
+ busy, onToggle: toggleLang, onSave: saveLang,
243
+ })),
244
+ React.createElement('div', { className: 'vsm-lsp-hint' }, '提示:打开 Lua/C# 文件即自动(惰性)启动对应语言服务器;未配置时编辑器功能不受影响,大纲回退内置解析。')))
245
+ } else if (tab === 'installed') {
246
+ body = panel('已安装扩展', React.createElement(React.Fragment, null,
247
+ React.createElement('div', { className: 'vsm-lsp-hint' }, '已安装的语言服务器扩展(存于 ~/.dsh/dsh-vscode-mode/extensions/)。语言服务器被自动发现为「扩展」源。'),
248
+ installed.length ? installed.map((ext) => React.createElement(InstalledCard, { key: ext.id, ext, busy, onUninstall: uninstallExt, onUpdate: updateExt }))
249
+ : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有安装扩展(去「市场」或指定本地 .vsix)')))
250
+ } else if (tab === 'market') {
251
+ body = panel('扩展市场', React.createElement(React.Fragment, null,
252
+ React.createElement('div', { className: 'vsm-lsp-form' },
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() } })),
254
+ React.createElement('button', { className: 'vsm-primary', disabled: busy === '__market', onClick: searchMarket }, busy === '__market' ? '搜索中…' : '搜索')),
255
+ React.createElement('div', { className: 'vsm-lsp-form' },
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) })),
257
+ React.createElement('button', { className: 'vsm-primary', disabled: busy === '__vsix', onClick: installLocal }, busy === '__vsix' ? '安装中…' : '安装本地 .vsix')),
258
+ React.createElement('div', { className: 'vsm-lsp-hint' }, '市场安装会下载并解包 VSIX 到扩展目录;含语言服务器(如 sumneko.lua)的扩展会被自动发现。'),
259
+ market.map((item) => React.createElement(MarketCard, { key: item.id, item, installed, busy, onInstall: installMarket }))))
260
+ } else {
261
+ body = panel('可用更新', React.createElement(React.Fragment, null,
262
+ React.createElement('div', { className: 'vsm-lsp-hint' }, '已装扩展与 Open VSX 最新版的差异。'),
263
+ updates.length ? updates.map((u) => React.createElement('article', { key: u.id, className: 'vsm-mcp-card' },
264
+ React.createElement('div', { className: 'vsm-mcp-card-head' },
265
+ React.createElement('div', { className: 'vsm-mcp-title' },
266
+ React.createElement('span', { className: 'vsm-mcp-dot error' }),
267
+ React.createElement('span', null, u.displayName || u.id),
268
+ React.createElement('span', { className: 'vsm-ext-ver' }, 'v' + u.current + ' → v' + u.latest)),
269
+ React.createElement('div', { className: 'vsm-mcp-actions' },
270
+ React.createElement('button', { className: 'vsm-primary vsm-small', disabled: busy === u.id, onClick: () => updateExt(u) }, busy === u.id ? '更新中…' : '更新')))))
271
+ : React.createElement('div', { className: 'vsm-mcp-empty' }, '全部已是最新')))
272
+ }
273
+ return React.createElement('div', null, header, nav, banner, body)
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'
@@ -12,6 +11,7 @@ import '../styles/mcp.css'
12
11
  import { availableOpeners, AUTO_OPEN_TOOL } from '../fileOpeners.js'
13
12
  import { SettingsContext } from '../settingsContext.js'
14
13
  import { KeybindingsPanel } from './KeybindingsPanel.js'
14
+ import { LspSettings } from './LspSettings.js'
15
15
  import { PerfSettings } from './PerfSettings.js'
16
16
 
17
17
  const EMPTY = { serverName: '', transport: 'stdio', command: '', args: '', cwd: '', url: '', headers: '' }
@@ -177,8 +177,13 @@ function GeneralSettings({ registry }) {
177
177
  React.createElement('p', null, '配置 DSH 对话中文件链接的打开工具。工具由当前运行时自动扫描。'),
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
- 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, '读取中…')),
181
- 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
+ ),
182
187
  )
183
188
  }
184
189
 
@@ -199,24 +204,89 @@ function CompatSection({ getSummary }) {
199
204
  React.createElement('span', { className: 'vsm-compat-name' }, item.name),
200
205
  React.createElement('span', { className: 'vsm-compat-note' }, item.note ?? (item.active ? '正常' : '未生效')),
201
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
+ )
202
212
  const warnings = report?.warnings ?? []
203
213
  const devForm = report?.devForm
204
214
  return React.createElement('section', { className: 'vsm-general-page' },
205
215
  React.createElement('h2', null, '兼容性'),
206
216
  React.createElement('p', null, '检测与其他插件 / DSH 版本的适配状态与已知问题。' + (report?.pluginVersion ? '(插件版本 ' + report.pluginVersion + ')' : '')),
207
217
  error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
208
- devForm?.enabled && React.createElement(React.Fragment, null,
209
- React.createElement('h3', { className: 'vsm-compat-h3' }, '开发形态'),
210
- 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'),
211
287
  ),
212
- React.createElement('h3', { className: 'vsm-compat-h3' }, '外部插件与服务'),
213
- renderItems(report?.external),
214
- React.createElement('h3', { className: 'vsm-compat-h3' }, '客户端适配'),
215
- renderItems(getSummary?.() ?? []),
216
- React.createElement('h3', { className: 'vsm-compat-h3' }, '护栏'),
217
- renderItems(report?.guards),
218
- React.createElement('h3', { className: 'vsm-compat-h3' }, '警告'),
219
- 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,
220
290
  )
221
291
  }
222
292
 
@@ -231,17 +301,6 @@ export function McpSettings(props) {
231
301
  const [error, setError] = React.useState('')
232
302
  const [tab, setTab] = React.useState('general')
233
303
  const [draft, setDraft] = React.useState(EMPTY)
234
- const [showForm, setShowForm] = React.useState(false)
235
- const [projectForm, setProjectForm] = React.useState(null)
236
- const [selectedPath, setSelectedPath] = React.useState('')
237
-
238
- React.useEffect(() => {
239
- if (!projects.length) {
240
- setSelectedPath('')
241
- return
242
- }
243
- if (!projects.some((project) => project.workspacePath === selectedPath)) setSelectedPath(projects[0].workspacePath)
244
- }, [projects, selectedPath])
245
304
 
246
305
  const loadAll = React.useCallback(() => {
247
306
  setLoading(true)
@@ -286,51 +345,55 @@ export function McpSettings(props) {
286
345
  const toggleProject = (p, s) => projectAction(p.workspacePath, s.serverName, 'mcp.projectToggle', { workspacePath: p.workspacePath, serverName: s.serverName, enabled: !s.enabled }, replaceProject(p.workspacePath))
287
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))
288
347
 
289
- const saveGlobal = () => {
348
+ /**
349
+ * 保存全局 MCP:成功后由回调关闭子面板。
350
+ * 作者 ddj 2026年08月27号
351
+ * @param {Function} close 关闭新增表单回调
352
+ */
353
+ const saveGlobal = (close) => {
290
354
  setBusy('save')
291
355
  setError('')
292
356
  rpc('mcp.save', { config: configOf(draft) }).then(finish).then((result) => {
293
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))
294
358
  setDraft(EMPTY)
295
- setShowForm(false)
359
+ close()
296
360
  }).catch((e) => setError(String(e))).finally(() => setBusy(''))
297
361
  }
298
362
 
299
- const saveProject = () => {
300
- 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) => {
301
370
  setBusy('project-save')
302
371
  setError('')
303
372
  const config = configOf(draft)
304
- rpc('mcp.projectSave', { workspacePath: projectForm.workspacePath, serverName: config.serverName, config }).then(finish).then((result) => {
305
- 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))
306
375
  setDraft(EMPTY)
307
- setProjectForm(null)
376
+ close()
308
377
  }).catch((e) => setError(String(e))).finally(() => setBusy(''))
309
378
  }
310
379
 
311
380
  const edit = (key, value) => setDraft((old) => ({ ...old, [key]: value }))
312
- const marketBody = React.createElement('div', { className: 'vsm-mcp-empty' }, 'MCP 市场暂未接入')
313
- const selectedProject = projects.find((project) => project.workspacePath === selectedPath)
314
- const projectBody = projects.length ? React.createElement(React.Fragment, null,
315
- React.createElement(ProjectPicker, { projects, value: selectedPath, onChange: setSelectedPath }),
316
- 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' }, '请选择项目'),
317
- ) : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有项目')
381
+ const resetDraft = () => setDraft(EMPTY)
318
382
  let body
319
383
  if (loading) body = React.createElement('div', { className: 'vsm-mcp-empty' }, '正在读取 MCP 服务…')
320
384
  else if (tab === 'general') body = React.createElement(GeneralSettings, { registry: openerRegistry })
321
385
  else if (tab === 'keys') body = React.createElement(KeybindingsPanel, null)
322
- 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'))
323
- 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 })
324
389
  else if (tab === 'compat') body = React.createElement(CompatSection, { getSummary: compatSummary })
325
- else if (tab === 'perf') body = React.createElement(PerfSettings, null)
326
- else body = marketBody
327
- 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
390
+ else if (tab === 'lsp') body = React.createElement(LspSettings, null)
391
+ else body = React.createElement(PerfSettings, null)
328
392
  return React.createElement('section', { className: 'vsm-mcp-page' },
329
- 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')),
330
- 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 === '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') }, '兼容性')),
331
395
  error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
332
396
  body,
333
- form,
334
397
  )
335
398
  }
336
399