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.
- package/lib/client.js +1117 -156
- package/lib/client.js.map +1 -1
- package/lib/index.js +447 -62
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/monaco/loader.ts +12 -1
- package/src/client/monaco/lsp/lspClient.ts +65 -11
- package/src/client/monaco/lsp/providers.ts +34 -1
- package/src/client/monaco/theme.ts +229 -0
- package/src/client/paths.ts +2 -0
- package/src/client/state/workspaceFoldCache.ts +95 -0
- package/src/client/styles/mcp.css +35 -12
- package/src/client/ui/EditorView.ts +21 -2
- package/src/client/ui/KeybindingsPanel.ts +8 -5
- package/src/client/ui/LspSettings.ts +53 -15
- package/src/client/ui/McpSettings.ts +110 -49
- package/src/client/ui/PerfSettings.ts +145 -34
- package/src/lsp/client.ts +29 -2
- package/src/lsp/manager.ts +15 -0
- package/src/lsp/providers.ts +203 -24
- package/src/lsp/rpc.ts +86 -4
- package/src/lsp/server.ts +80 -3
- package/src/paths.ts +7 -0
- package/src/rpc.ts +4 -1
- package/src/shared/lsp.ts +24 -0
- package/src/shared/rpc.ts +6 -2
|
@@ -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
|
|
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:
|
|
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('
|
|
182
|
-
|
|
183
|
-
React.createElement('
|
|
184
|
-
React.createElement('
|
|
185
|
-
|
|
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
|
|
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) {
|
|
149
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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('
|
|
182
|
-
|
|
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.
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
359
|
+
close()
|
|
297
360
|
}).catch((e) => setError(String(e))).finally(() => setBusy(''))
|
|
298
361
|
}
|
|
299
362
|
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
306
|
-
setProjects((old) => old.map((p) => p.workspacePath ===
|
|
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
|
-
|
|
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
|
|
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 === '
|
|
324
|
-
|
|
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
|
|
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 服务。'))
|
|
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 === '
|
|
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
|
|