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.
- package/LICENSE +21 -0
- package/lib/client.js +1229 -124
- package/lib/client.js.map +1 -1
- package/lib/index.js +1959 -52
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/events.ts +11 -0
- package/src/client/index.ts +15 -2
- package/src/client/monaco/lsp/index.ts +30 -0
- package/src/client/monaco/lsp/lspClient.ts +192 -0
- package/src/client/monaco/lsp/providers.ts +229 -0
- package/src/client/outline/OutlinePanel.tsx +1 -1
- package/src/client/outline/lspSource.ts +49 -0
- package/src/client/outline/parse.ts +31 -1
- package/src/client/outline/types.ts +2 -0
- package/src/client/paths.ts +2 -0
- package/src/client/state/workspaceFoldCache.ts +95 -0
- package/src/client/styles/editor.css +15 -0
- package/src/client/styles/mcp.css +50 -12
- package/src/client/ui/EditorView.ts +55 -1
- package/src/client/ui/KeybindingsPanel.ts +8 -5
- package/src/client/ui/LspSettings.ts +274 -0
- package/src/client/ui/McpSettings.ts +112 -49
- package/src/client/ui/PerfSettings.ts +145 -34
- package/src/fileOpenSettings.ts +1 -0
- package/src/index.ts +20 -2
- package/src/lsp/client.ts +205 -0
- package/src/lsp/config.ts +107 -0
- package/src/lsp/extmgr.ts +208 -0
- package/src/lsp/jsonrpc.ts +86 -0
- package/src/lsp/manager.ts +133 -0
- package/src/lsp/providers.ts +235 -0
- package/src/lsp/rpc.ts +334 -0
- package/src/lsp/server.ts +346 -0
- package/src/lsp/settings.ts +44 -0
- package/src/lsp/transport.ts +143 -0
- package/src/lsp/uri.ts +62 -0
- package/src/lsp/zip.ts +82 -0
- package/src/rpc.ts +11 -2
- package/src/shared/lsp.ts +138 -0
- 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
|
-
*
|
|
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'
|
|
@@ -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('
|
|
181
|
-
|
|
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.
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
359
|
+
close()
|
|
296
360
|
}).catch((e) => setError(String(e))).finally(() => setBusy(''))
|
|
297
361
|
}
|
|
298
362
|
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
305
|
-
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))
|
|
306
375
|
setDraft(EMPTY)
|
|
307
|
-
|
|
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
|
|
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 === '
|
|
323
|
-
|
|
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 === '
|
|
326
|
-
else body =
|
|
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 服务。'))
|
|
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 === '
|
|
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
|
|