dsh-vscode-mode 0.1.35 → 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +389 -128
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
- 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/KeybindingsPanel.ts +8 -5
- package/src/client/ui/LspSettings.ts +14 -8
- package/src/client/ui/McpSettings.ts +110 -49
- package/src/client/ui/PerfSettings.ts +145 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 dsh-better-sidebar 侧边栏与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
package/src/client/paths.ts
CHANGED
|
@@ -22,6 +22,8 @@ export const CACHE_KEY = {
|
|
|
22
22
|
search: 'edrv.search.v1.',
|
|
23
23
|
/** 侧边栏提示已关闭标记(全局,无会话)。 */
|
|
24
24
|
sideHint: 'edrv.side-hint-dismissed',
|
|
25
|
+
/** 「性能优化」页工作区栏目展开状态(workspaceFoldCache v1,全局不按会话)。 */
|
|
26
|
+
workspaceFold: 'edrv.ws-fold.v1.',
|
|
25
27
|
} as const
|
|
26
28
|
|
|
27
29
|
/** 按前缀拼会话键(侧边栏/编辑器等需要布局段时自行拼接后传入)。 */
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 「性能优化」页工作区栏目展开状态持久化(纯函数,可单测)。
|
|
3
|
+
* 只持久化「已展开」的工作区键集合(默认全部折叠),对齐 explorerCache 的做法:
|
|
4
|
+
* 不缓存会话条目本身,条目由 edrv.perf.inventory 实时返回。
|
|
5
|
+
* 键前缀统一走 paths.ts PathConst(CACHE_KEY.workspaceFold)。
|
|
6
|
+
* 作者 ddj 2026-09-02
|
|
7
|
+
*/
|
|
8
|
+
import { CACHE_KEY } from '../paths.js'
|
|
9
|
+
|
|
10
|
+
/** localStorage 键(全局,不按会话隔离)。 */
|
|
11
|
+
const STORE_KEY = CACHE_KEY.workspaceFold
|
|
12
|
+
|
|
13
|
+
/** 展开键容量上限:超出丢弃尾部,防止无限增长撑爆配额。 */
|
|
14
|
+
export const FOLD_CAP = 300
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 归一化工作区键:仅去首尾空白(工作区键非文件系统路径,不做斜杠转换)。
|
|
18
|
+
* @author ddj 2026年09月02号
|
|
19
|
+
* @param key 工作区标识
|
|
20
|
+
* @returns 归一化键,空值返回 ''
|
|
21
|
+
*/
|
|
22
|
+
export function normalizeFoldKey(key: string | undefined | null): string {
|
|
23
|
+
return String(key ?? '').trim()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 裁剪展开键集:去重、丢弃空值、超容量丢弃尾部(保持传入顺序)。
|
|
28
|
+
* @author ddj 2026年09月02号
|
|
29
|
+
* @param expanded 原始展开键集合
|
|
30
|
+
* @returns 规范化后的键数组
|
|
31
|
+
*/
|
|
32
|
+
export function sanitizeFolded(expanded: unknown): string[] {
|
|
33
|
+
if (!Array.isArray(expanded)) return []
|
|
34
|
+
const seen = new Set<string>()
|
|
35
|
+
const out: string[] = []
|
|
36
|
+
for (const item of expanded) {
|
|
37
|
+
const key = normalizeFoldKey(typeof item === 'string' ? item : '')
|
|
38
|
+
if (!key || seen.has(key)) continue
|
|
39
|
+
seen.add(key)
|
|
40
|
+
out.push(key)
|
|
41
|
+
if (out.length >= FOLD_CAP) break
|
|
42
|
+
}
|
|
43
|
+
return out
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 序列化展开状态(JSON,带 schema 版本号)。
|
|
48
|
+
* @author ddj 2026年09月02号
|
|
49
|
+
* @param expanded 已展开的项目键集合
|
|
50
|
+
* @returns JSON 字符串
|
|
51
|
+
*/
|
|
52
|
+
export function serializeFold(expanded: string[]): string {
|
|
53
|
+
return JSON.stringify({ v: 1, expanded: sanitizeFolded(expanded) })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 解析缓存文本;损坏/格式不符 → 空集合。
|
|
58
|
+
* @author ddj 2026年09月02号
|
|
59
|
+
* @param text localStorage 原文
|
|
60
|
+
* @returns 已展开键集合
|
|
61
|
+
*/
|
|
62
|
+
export function parseFold(text: string | null): string[] {
|
|
63
|
+
if (!text) return []
|
|
64
|
+
try {
|
|
65
|
+
const data = JSON.parse(text)
|
|
66
|
+
if (!data || typeof data !== 'object') return []
|
|
67
|
+
return sanitizeFolded(data.expanded)
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return []
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 读取展开状态(localStorage 不可用 → 空集合)。
|
|
75
|
+
* @author ddj 2026年09月02号
|
|
76
|
+
* @returns 已展开键集合
|
|
77
|
+
*/
|
|
78
|
+
export function foldLoad(): string[] {
|
|
79
|
+
try {
|
|
80
|
+
return parseFold(window.localStorage.getItem(STORE_KEY))
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return []
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 写入展开状态(配额满/隐私模式失败静默)。
|
|
88
|
+
* @author ddj 2026年09月02号
|
|
89
|
+
* @param expanded 已展开键集合
|
|
90
|
+
*/
|
|
91
|
+
export function foldSave(expanded: string[]): void {
|
|
92
|
+
try {
|
|
93
|
+
window.localStorage.setItem(STORE_KEY, serializeFold(expanded))
|
|
94
|
+
} catch (error) { /* 配额满/隐私模式忽略 */ }
|
|
95
|
+
}
|
|
@@ -10,18 +10,36 @@
|
|
|
10
10
|
.vsm-primary:hover{background:var(--dsw-alias-button-primary-hover,#5a46e6)}
|
|
11
11
|
.vsm-primary:disabled{opacity:.55;cursor:default}
|
|
12
12
|
|
|
13
|
-
/*
|
|
13
|
+
/* MCP 管理下的二级子页签:我的 MCP / 项目 MCP / MCP 市场 */
|
|
14
|
+
.vsm-mcp-subhead{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-top:-4px}
|
|
15
|
+
.vsm-mcp-subtabs{display:flex;gap:18px;border-bottom:1px solid var(--dsw-alias-border-l1,#2e2e2e);flex:1}
|
|
16
|
+
.vsm-mcp-subtabs button{border:0;background:none;color:var(--dsw-alias-label-tertiary,#9aa5b1);padding:7px 2px 9px;cursor:pointer;font-size:13px;position:relative}
|
|
17
|
+
.vsm-mcp-subtabs button:hover{color:var(--dsw-alias-label-secondary,#c8c8c8)}
|
|
18
|
+
.vsm-mcp-subtabs button.active{color:var(--dsw-alias-label-primary,#fff);font-weight:500}
|
|
19
|
+
.vsm-mcp-subtabs button.active::after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--dsw-alias-brand-primary,#6d55ff);border-radius:2px}
|
|
20
|
+
|
|
21
|
+
/* 顶层 Tab:通用 / 快捷键 / MCP 管理 / 语言服务器 / 性能优化 / 兼容性 */
|
|
14
22
|
.vsm-mcp-tabs{display:flex;gap:24px;border-bottom:1px solid var(--dsw-alias-border-l2,#3a3a3a)}
|
|
15
23
|
.vsm-mcp-tabs button{border:0;background:none;color:var(--dsw-alias-label-tertiary,#9aa5b1);padding:9px 2px 11px;cursor:pointer;font-size:14px;position:relative}
|
|
16
24
|
.vsm-mcp-tabs button:hover{color:var(--dsw-alias-label-secondary,#c8c8c8)}
|
|
17
25
|
.vsm-mcp-tabs button.active{color:var(--dsw-alias-label-primary,#fff);font-weight:500}
|
|
18
26
|
.vsm-mcp-tabs button.active::after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--dsw-alias-brand-primary,#6d55ff);border-radius:2px}
|
|
19
27
|
|
|
28
|
+
/* 统一线框面板:圆角矩形边框圈住各设置分区,提升可读性。
|
|
29
|
+
用法:<section class="vsm-panel"> + 可选 <h3 class="vsm-panel-title">。 */
|
|
30
|
+
.vsm-panel{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:12px;background:var(--dsw-alias-bg-layer-2,transparent);overflow:hidden}
|
|
31
|
+
.vsm-panel-pad{padding:14px 16px}
|
|
32
|
+
.vsm-panel-title{margin:0;padding:10px 16px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-secondary,#c8c8c8);background:var(--dsw-alias-bg-mask-1,#ffffff0a);border-bottom:1px solid var(--dsw-alias-border-l1,#333)}
|
|
33
|
+
.vsm-panel-body{padding:6px 16px}
|
|
34
|
+
.vsm-panel-body>*:first-child{margin-top:8px}
|
|
35
|
+
.vsm-panel-body>*:last-child{margin-bottom:10px}
|
|
36
|
+
|
|
20
37
|
/* 通用设置 */
|
|
21
|
-
.vsm-general-page{display:flex;flex-direction:column;gap:
|
|
38
|
+
.vsm-general-page{display:flex;flex-direction:column;gap:14px;max-width:720px}
|
|
22
39
|
.vsm-general-page h2{margin:0;font-size:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
23
|
-
.vsm-general-page p{margin:0
|
|
40
|
+
.vsm-general-page p{margin:0;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:13px}
|
|
24
41
|
.vsm-general-row{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:14px 0;border-bottom:1px solid var(--dsw-alias-border-l1,#333);color:var(--dsw-alias-label-primary,#eee);font-size:14px}
|
|
42
|
+
.vsm-general-row:last-child{border-bottom:0}
|
|
25
43
|
.vsm-general-row select{min-width:220px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:var(--dsw-alias-bg-layer-1,#2b2b2b);color:var(--dsw-alias-label-primary,#eee);font:inherit}
|
|
26
44
|
.vsm-general-row small{color:var(--dsw-alias-label-tertiary,#9aa5b1)}
|
|
27
45
|
|
|
@@ -94,8 +112,7 @@
|
|
|
94
112
|
.vsm-mcp-dialog-actions button{padding:8px 14px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:none;color:var(--dsw-alias-label-primary,#eee);cursor:pointer;font-size:13px}
|
|
95
113
|
.vsm-mcp-dialog-actions button:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
|
|
96
114
|
|
|
97
|
-
/*
|
|
98
|
-
.vsm-compat-h3{margin:16px 0 2px;font-size:14px;font-weight:600;color:var(--dsw-alias-label-secondary,#c8c8c8)}
|
|
115
|
+
/* 兼容性条目(面板标题改用 .vsm-panel-title) */
|
|
99
116
|
.vsm-compat-item{display:flex;align-items:baseline;justify-content:space-between;gap:16px;padding:10px 0;border-bottom:1px solid var(--dsw-alias-border-l1,#333);font-size:13px;color:var(--dsw-alias-label-primary,#eee)}
|
|
100
117
|
.vsm-compat-item.ok .vsm-compat-name::before{content:"● ";color:var(--dsw-alias-state-success-primary,#38bd75)}
|
|
101
118
|
.vsm-compat-item.warn .vsm-compat-name::before{content:"● ";color:var(--dsw-alias-state-warn-primary,#e5b93f)}
|
|
@@ -139,6 +156,7 @@
|
|
|
139
156
|
|
|
140
157
|
/* 会话性能(PerfSettings 子 Tab) */
|
|
141
158
|
.vsm-perf-page{display:flex;flex-direction:column;gap:14px;max-width:920px}
|
|
159
|
+
.vsm-perf-page>.vsm-panel-body>*:first-child{margin-top:0}
|
|
142
160
|
.vsm-perf-page h2{margin:0;font-size:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
143
161
|
.vsm-perf-page p{margin:0;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:13px;line-height:1.5}
|
|
144
162
|
.vsm-perf-cards{display:flex;gap:12px;flex-wrap:wrap}
|
|
@@ -148,8 +166,19 @@
|
|
|
148
166
|
.vsm-perf-card b.big{color:var(--dsw-alias-state-error-primary,#e05252)}
|
|
149
167
|
.vsm-perf-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
150
168
|
.vsm-perf-selcount{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);margin:0 4px}
|
|
169
|
+
/* 工作区列表分页器 */
|
|
170
|
+
.vsm-perf-pager{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-top:4px}
|
|
171
|
+
.vsm-perf-pager-text{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1)}
|
|
172
|
+
.vsm-perf-pager-actions{display:flex;gap:8px}
|
|
173
|
+
.vsm-perf-ws-list{display:flex;flex-direction:column;gap:8px}
|
|
151
174
|
.vsm-perf-ws{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;overflow:hidden}
|
|
152
|
-
.vsm-perf-ws-head{display:flex;
|
|
175
|
+
.vsm-perf-ws-head{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--dsw-alias-bg-layer-1,#262626);cursor:pointer;user-select:none}
|
|
176
|
+
.vsm-perf-ws-head:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
|
|
177
|
+
.vsm-perf-ws-head:focus-visible{outline:1px solid var(--dsw-alias-brand-primary,#6d55ff);outline-offset:-1px}
|
|
178
|
+
.vsm-perf-ws.expanded .vsm-perf-ws-head{border-bottom:1px solid var(--dsw-alias-border-l2,#3a3a3a)}
|
|
179
|
+
.vsm-perf-ws-caret{color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:12px;flex-shrink:0;transition:transform .15s;display:inline-block}
|
|
180
|
+
.vsm-perf-ws-caret.open{transform:rotate(90deg)}
|
|
181
|
+
.vsm-perf-ws .vsm-perf-ws-name{flex:1}
|
|
153
182
|
.vsm-perf-ws-name{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6);font-family:var(--ds-font-family-code,ui-monospace,monospace);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
154
183
|
.vsm-perf-ws-meta{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);flex-shrink:0}
|
|
155
184
|
.vsm-perf-rows{display:flex;flex-direction:column}
|
|
@@ -163,15 +192,9 @@
|
|
|
163
192
|
.vsm-perf-size.big{color:var(--dsw-alias-state-error-primary,#e05252);font-weight:600}
|
|
164
193
|
.vsm-perf-mtime{width:44px;flex-shrink:0;color:var(--dsw-alias-label-tertiary,#9aa5b1)}
|
|
165
194
|
.vsm-perf-active{width:44px;flex-shrink:0;font-size:11px;color:var(--dsw-alias-state-warn-primary,#e5b93f);text-align:right}
|
|
166
|
-
.vsm-perf-moved{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-2,transparent)}
|
|
167
|
-
.vsm-perf-moved h3{margin:0 0 8px;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
168
|
-
.vsm-perf-config{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-2,transparent)}
|
|
169
|
-
.vsm-perf-config h3{margin:0 0 8px;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
170
195
|
.vsm-perf-config-state{display:flex;align-items:center;gap:12px;margin:10px 0}
|
|
171
196
|
.vsm-perf-note{font-size:12px;color:var(--dsw-alias-state-warn-primary,#e5b93f)}
|
|
172
197
|
.vsm-perf-block{margin:8px 0 0;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:8px;background:var(--dsw-alias-bg-base,#111);color:var(--dsw-alias-label-secondary,#c8c8c8);font-size:11px;line-height:1.5;font-family:var(--ds-font-family-code,ui-monospace,monospace);white-space:pre-wrap;overflow-x:auto;max-width:100%}
|
|
173
|
-
.vsm-perf-guide{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-2,transparent)}
|
|
174
|
-
.vsm-perf-guide h3{margin:0 0 8px;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
175
198
|
.vsm-perf-modal{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:var(--dsw-alias-bg-mask-1,#000000aa);z-index:1000;padding:20px}
|
|
176
199
|
.vsm-perf-modal-card{width:min(560px,92vw);max-height:72vh;display:flex;flex-direction:column;gap:12px;padding:18px 20px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#1f1f1f);color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
177
200
|
.vsm-perf-modal-card h3{margin:0;font-size:16px}
|
|
@@ -178,10 +178,13 @@ export function KeybindingsPanel() {
|
|
|
178
178
|
unavailable && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, '设置服务暂不可用,当前使用默认键位。'),
|
|
179
179
|
error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
|
|
180
180
|
message && React.createElement('div', { className: 'vsm-kb-message' }, message),
|
|
181
|
-
React.createElement('
|
|
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
|
}
|
|
@@ -226,23 +226,29 @@ export function LspSettings() {
|
|
|
226
226
|
React.createElement('button', { className: 'vsm-primary', onClick: refresh }, '↻ 刷新'))
|
|
227
227
|
const banner = error ? React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error) : null
|
|
228
228
|
|
|
229
|
+
/** 线框面板容器:标题 + 内容体。 */
|
|
230
|
+
const panel = (title, children) => React.createElement('section', { className: 'vsm-panel' },
|
|
231
|
+
React.createElement('h3', { className: 'vsm-panel-title' }, title),
|
|
232
|
+
React.createElement('div', { className: 'vsm-panel-body' }, children),
|
|
233
|
+
)
|
|
234
|
+
|
|
229
235
|
let body = null
|
|
230
236
|
if (tab === 'servers') {
|
|
231
|
-
body = React.createElement(
|
|
237
|
+
body = panel('语言服务器', React.createElement(React.Fragment, null,
|
|
232
238
|
LANGUAGES.map((lang) => React.createElement(LangCard, {
|
|
233
239
|
key: lang.id, lang,
|
|
234
240
|
config: config[lang.id] ?? {},
|
|
235
241
|
status: servers.find((s) => s.languageId === lang.id),
|
|
236
242
|
busy, onToggle: toggleLang, onSave: saveLang,
|
|
237
243
|
})),
|
|
238
|
-
React.createElement('div', { className: 'vsm-lsp-hint' }, '提示:打开 Lua/C# 文件即自动(惰性)启动对应语言服务器;未配置时编辑器功能不受影响,大纲回退内置解析。'))
|
|
244
|
+
React.createElement('div', { className: 'vsm-lsp-hint' }, '提示:打开 Lua/C# 文件即自动(惰性)启动对应语言服务器;未配置时编辑器功能不受影响,大纲回退内置解析。')))
|
|
239
245
|
} else if (tab === 'installed') {
|
|
240
|
-
body = React.createElement(
|
|
246
|
+
body = panel('已安装扩展', React.createElement(React.Fragment, null,
|
|
241
247
|
React.createElement('div', { className: 'vsm-lsp-hint' }, '已安装的语言服务器扩展(存于 ~/.dsh/dsh-vscode-mode/extensions/)。语言服务器被自动发现为「扩展」源。'),
|
|
242
248
|
installed.length ? installed.map((ext) => React.createElement(InstalledCard, { key: ext.id, ext, busy, onUninstall: uninstallExt, onUpdate: updateExt }))
|
|
243
|
-
: React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有安装扩展(去「市场」或指定本地 .vsix)'))
|
|
249
|
+
: React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有安装扩展(去「市场」或指定本地 .vsix)')))
|
|
244
250
|
} else if (tab === 'market') {
|
|
245
|
-
body = React.createElement(
|
|
251
|
+
body = panel('扩展市场', React.createElement(React.Fragment, null,
|
|
246
252
|
React.createElement('div', { className: 'vsm-lsp-form' },
|
|
247
253
|
React.createElement('label', null, React.createElement('span', null, '搜索 Open VSX 市场'), React.createElement('input', { value: query, placeholder: '如 sumneko.lua / lua language server', onChange: (e) => setQuery(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter') searchMarket() } })),
|
|
248
254
|
React.createElement('button', { className: 'vsm-primary', disabled: busy === '__market', onClick: searchMarket }, busy === '__market' ? '搜索中…' : '搜索')),
|
|
@@ -250,9 +256,9 @@ export function LspSettings() {
|
|
|
250
256
|
React.createElement('label', null, React.createElement('span', null, '安装本地 .vsix 文件(服务器绝对路径)'), React.createElement('input', { value: vsixPath, placeholder: '如 C:/Downloads/sumneko.lua-3.19.1.vsix', onChange: (e) => setVsixPath(e.target.value) })),
|
|
251
257
|
React.createElement('button', { className: 'vsm-primary', disabled: busy === '__vsix', onClick: installLocal }, busy === '__vsix' ? '安装中…' : '安装本地 .vsix')),
|
|
252
258
|
React.createElement('div', { className: 'vsm-lsp-hint' }, '市场安装会下载并解包 VSIX 到扩展目录;含语言服务器(如 sumneko.lua)的扩展会被自动发现。'),
|
|
253
|
-
market.map((item) => React.createElement(MarketCard, { key: item.id, item, installed, busy, onInstall: installMarket })))
|
|
259
|
+
market.map((item) => React.createElement(MarketCard, { key: item.id, item, installed, busy, onInstall: installMarket }))))
|
|
254
260
|
} else {
|
|
255
|
-
body = React.createElement(
|
|
261
|
+
body = panel('可用更新', React.createElement(React.Fragment, null,
|
|
256
262
|
React.createElement('div', { className: 'vsm-lsp-hint' }, '已装扩展与 Open VSX 最新版的差异。'),
|
|
257
263
|
updates.length ? updates.map((u) => React.createElement('article', { key: u.id, className: 'vsm-mcp-card' },
|
|
258
264
|
React.createElement('div', { className: 'vsm-mcp-card-head' },
|
|
@@ -262,7 +268,7 @@ export function LspSettings() {
|
|
|
262
268
|
React.createElement('span', { className: 'vsm-ext-ver' }, 'v' + u.current + ' → v' + u.latest)),
|
|
263
269
|
React.createElement('div', { className: 'vsm-mcp-actions' },
|
|
264
270
|
React.createElement('button', { className: 'vsm-primary vsm-small', disabled: busy === u.id, onClick: () => updateExt(u) }, busy === u.id ? '更新中…' : '更新')))))
|
|
265
|
-
: React.createElement('div', { className: 'vsm-mcp-empty' }, '全部已是最新'))
|
|
271
|
+
: React.createElement('div', { className: 'vsm-mcp-empty' }, '全部已是最新')))
|
|
266
272
|
}
|
|
267
273
|
return React.createElement('div', null, header, nav, banner, body)
|
|
268
274
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
/**
|
|
3
|
-
* dsh-vscode-mode client — VSCodeMode 设置区:通用 / 快捷键 / MCP
|
|
4
|
-
*
|
|
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
|
|