dsh-vscode-mode 0.1.30 → 0.1.35
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/README.md +30 -0
- package/lib/client.js +1439 -46
- package/lib/client.js.map +1 -1
- package/lib/index.js +3519 -222
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/capture.ts +3 -0
- package/src/client/events.ts +11 -0
- package/src/client/index.ts +20 -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 +36 -0
- package/src/client/sidebar/panels/FileExplorer.ts +95 -30
- package/src/client/sidebar/panels/SearchPanel.ts +11 -2
- package/src/client/state/explorerCache.ts +3 -1
- package/src/client/state/explorerEntriesCache.ts +172 -0
- package/src/client/state/viewStateCache.ts +3 -1
- package/src/client/styles/editor.css +16 -0
- package/src/client/styles/mcp.css +63 -0
- package/src/client/ui/EditorView.ts +61 -6
- package/src/client/ui/LspSettings.ts +268 -0
- package/src/client/ui/McpSettings.ts +5 -1
- package/src/client/ui/PerfSettings.ts +242 -0
- package/src/client/ui/PerfSizeBadge.ts +54 -0
- package/src/compat.ts +3 -2
- package/src/fileOpenSettings.ts +1 -0
- package/src/index.ts +25 -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/paths.ts +284 -0
- package/src/perf.ts +340 -0
- package/src/perfPatch.ts +76 -0
- package/src/routes.ts +7 -12
- package/src/rpc.ts +184 -36
- package/src/search/content.ts +14 -7
- package/src/search/ripgrep.ts +54 -1
- package/src/search/types.ts +4 -1
- package/src/shared/lsp.ts +138 -0
- package/src/shared/rpc.ts +103 -2
- package/src/store.ts +2 -2
- package/src/treeIndex.ts +397 -0
- package/src/treeRead.ts +87 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — PerfSettings:「性能优化」设置子页(会话卫生 + 压缩调优)。
|
|
4
|
+
* - 会话盘点:全工作区/每会话体积、活跃标记,按体积排序;
|
|
5
|
+
* - 移出到归档(先规划→确认→执行→可恢复)与归档清除;
|
|
6
|
+
* - DSH 内置压缩调优:写 profile patch(compaction-basic / tool-result-pruner 更低阈值,
|
|
7
|
+
* 带备份与撤销,需重启生效);
|
|
8
|
+
* - 侧车摘要用法提示(agent/脚本取关键字段,不整份读 .dsh-edit-review.json)。
|
|
9
|
+
* 作者 ddj 2026-09-02
|
|
10
|
+
*/
|
|
11
|
+
import React from 'react'
|
|
12
|
+
import { rpc } from '../rpc.js'
|
|
13
|
+
import '../styles/mcp.css'
|
|
14
|
+
|
|
15
|
+
/** 字节数人类可读。 */
|
|
16
|
+
function formatBytes(n) {
|
|
17
|
+
if (!n) return '0B'
|
|
18
|
+
if (n >= 1024 * 1024) return (n / (1024 * 1024)).toFixed(1) + 'MB'
|
|
19
|
+
if (n >= 1024) return (n / 1024).toFixed(0) + 'KB'
|
|
20
|
+
return n + 'B'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 会话行 key:workspaceKey \u0000 sessionId。 */
|
|
24
|
+
const keyOf = (ws, id) => ws + '\u0000' + id
|
|
25
|
+
|
|
26
|
+
/** 单行会话。 */
|
|
27
|
+
function SessionRow({ row, checked, disabled, onToggle }) {
|
|
28
|
+
const age = Math.max(0, (Date.now() - row.mtime) / (24 * 3600 * 1000))
|
|
29
|
+
const when = age < 1 ? Math.round(age * 24) + 'h' : Math.round(age) + 'd'
|
|
30
|
+
return React.createElement('label', { className: 'vsm-perf-row' },
|
|
31
|
+
React.createElement('input', { type: 'checkbox', checked, disabled, onChange: onToggle }),
|
|
32
|
+
React.createElement('span', { className: 'vsm-perf-cell vsm-perf-id', title: row.sessionId }, row.sessionId),
|
|
33
|
+
React.createElement('span', { className: 'vsm-perf-cell vsm-perf-size ' + (row.bytes >= 1024 * 1024 ? 'big' : '') }, formatBytes(row.bytes)),
|
|
34
|
+
React.createElement('span', { className: 'vsm-perf-cell vsm-perf-mtime', title: new Date(row.mtime).toLocaleString() }, when),
|
|
35
|
+
row.active ? React.createElement('span', { className: 'vsm-perf-cell vsm-perf-active' }, '活跃') : null,
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 会话卫生 + 压缩调优设置子页。
|
|
41
|
+
* @author ddj 2026年09月02号
|
|
42
|
+
*/
|
|
43
|
+
export function PerfSettings() {
|
|
44
|
+
const [inv, setInv] = React.useState(null)
|
|
45
|
+
const [error, setError] = React.useState('')
|
|
46
|
+
const [busy, setBusy] = React.useState('')
|
|
47
|
+
const [selected, setSelected] = React.useState({})
|
|
48
|
+
const [confirm, setConfirm] = React.useState(null)
|
|
49
|
+
const [moved, setMoved] = React.useState(null)
|
|
50
|
+
const [config, setConfig] = React.useState(null)
|
|
51
|
+
|
|
52
|
+
const load = React.useCallback(() => {
|
|
53
|
+
setError('')
|
|
54
|
+
Promise.all([rpc('edrv.perf.inventory', {}), rpc('edrv.perf.configGet', {})]).then(([i, c]) => {
|
|
55
|
+
if (!i.ok) throw new Error(i.error)
|
|
56
|
+
if (!c.ok) throw new Error(c.error)
|
|
57
|
+
setInv(i)
|
|
58
|
+
setConfig(c)
|
|
59
|
+
}).catch((e) => setError(String(e)))
|
|
60
|
+
}, [])
|
|
61
|
+
|
|
62
|
+
React.useEffect(load, [load])
|
|
63
|
+
|
|
64
|
+
const action = (id, method, args, patch) => {
|
|
65
|
+
setBusy(id)
|
|
66
|
+
setError('')
|
|
67
|
+
return rpc(method, args).then((res) => {
|
|
68
|
+
if (!res.ok) throw new Error(res.error)
|
|
69
|
+
if (patch) patch(res)
|
|
70
|
+
return res
|
|
71
|
+
}).catch((e) => setError(String(e))).finally(() => setBusy(''))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 圈选 ≥ 1MB 的非活跃会话。 */
|
|
75
|
+
const selectBig = () => {
|
|
76
|
+
if (!inv) return
|
|
77
|
+
const next = {}
|
|
78
|
+
for (const s of inv.sessions) {
|
|
79
|
+
if (s.bytes >= 1024 * 1024 && !s.active) next[keyOf(s.workspaceKey, s.sessionId)] = true
|
|
80
|
+
}
|
|
81
|
+
setSelected(next)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 清空选择。 */
|
|
85
|
+
const clearSel = () => setSelected({})
|
|
86
|
+
|
|
87
|
+
/** 规划移出所选(跨工作区按 sessionIds 一次规划)。 */
|
|
88
|
+
const planMove = () => {
|
|
89
|
+
const sessionIds = Object.keys(selected).map((k) => k.slice(k.indexOf('\u0000') + 1))
|
|
90
|
+
if (!sessionIds.length) return
|
|
91
|
+
action('plan', 'edrv.perf.movePlan', { sessionIds }).then((res) => {
|
|
92
|
+
if (res.items?.length) setConfirm(res)
|
|
93
|
+
else setError('所选会话没有可移出的项(活跃会话已排除)')
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 确认移出:按工作区分组逐组执行,成功后刷新并记录可恢复项。 */
|
|
98
|
+
const doMove = () => {
|
|
99
|
+
const grouped = {}
|
|
100
|
+
for (const item of confirm.items) {
|
|
101
|
+
const list = grouped[item.workspaceKey] ?? (grouped[item.workspaceKey] = [])
|
|
102
|
+
list.push(item.sessionId)
|
|
103
|
+
}
|
|
104
|
+
const jobs = Object.entries(grouped).map(([ws, sessionIds]) => rpc('edrv.perf.moveOut', { workspaceKey: ws, sessionIds }))
|
|
105
|
+
Promise.all(jobs).then((results) => {
|
|
106
|
+
const allMoved = []
|
|
107
|
+
const failures = []
|
|
108
|
+
for (const res of results) {
|
|
109
|
+
if (!res.ok) { failures.push(res.error); continue }
|
|
110
|
+
allMoved.push(...(res.moved ?? []))
|
|
111
|
+
failures.push(...(res.failures ?? []).map((f) => f.sessionId + ': ' + f.error))
|
|
112
|
+
}
|
|
113
|
+
setMoved({ moved: allMoved, failures, reclaimedBytes: allMoved.reduce((s, i) => s + i.bytes, 0) })
|
|
114
|
+
setConfirm(null)
|
|
115
|
+
setSelected({})
|
|
116
|
+
load()
|
|
117
|
+
}).catch((e) => setError(String(e)))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 恢复单个已移出会话。 */
|
|
121
|
+
const doRestore = (item) => {
|
|
122
|
+
action('restore', 'edrv.perf.restore', { workspaceKey: item.workspaceKey, sessionId: item.sessionId }).then(() => {
|
|
123
|
+
setMoved((old) => old ? { ...old, moved: old.moved.filter((m) => !(m.workspaceKey === item.workspaceKey && m.sessionId === item.sessionId)) } : old)
|
|
124
|
+
load()
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 清除归档区早于 N 天的会话(破坏性)。 */
|
|
129
|
+
const doPurge = () => {
|
|
130
|
+
if (!window.confirm('确认永久删除归档区中 30 天前的会话?此操作不可恢复(仅归档区,不影响当前 sessions)。')) return
|
|
131
|
+
action('purge', 'edrv.perf.purgeArchive', { olderThanDays: 30 }).then((res) => {
|
|
132
|
+
setError('已清除 ' + (res.removed ?? 0) + ' 个归档会话,释放 ' + formatBytes(res.reclaimedBytes ?? 0))
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 应用压缩调优到 profile patch。 */
|
|
137
|
+
const applyConfig = () => {
|
|
138
|
+
action('apply', 'edrv.perf.configApply', {}).then((res) => {
|
|
139
|
+
setError('已写入压缩调优配置(备份 ' + res.backup + '),重启 DSH 后生效')
|
|
140
|
+
load()
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 撤销压缩调优。 */
|
|
145
|
+
const undoConfig = () => {
|
|
146
|
+
action('undo', 'edrv.perf.configUndo', {}).then(() => {
|
|
147
|
+
setError('已撤销压缩调优配置(恢复备份)')
|
|
148
|
+
load()
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const totals = inv?.totals
|
|
153
|
+
const summaryCards = totals ? React.createElement('div', { className: 'vsm-perf-cards' },
|
|
154
|
+
React.createElement('div', { className: 'vsm-perf-card' }, React.createElement('span', null, '工作区'), React.createElement('b', null, String(totals.workspaces))),
|
|
155
|
+
React.createElement('div', { className: 'vsm-perf-card' }, React.createElement('span', null, '会话数'), React.createElement('b', null, String(totals.sessions))),
|
|
156
|
+
React.createElement('div', { className: 'vsm-perf-card' }, React.createElement('span', null, '压缩态总体积'), React.createElement('b', { className: totals.totalBytes >= 50 * 1024 * 1024 ? 'big' : '' }, formatBytes(totals.totalBytes))),
|
|
157
|
+
) : null
|
|
158
|
+
|
|
159
|
+
const workspaceBlocks = inv?.workspaces ? inv.workspaces.map((ws) => {
|
|
160
|
+
const rows = inv.sessions.filter((s) => s.workspaceKey === ws.workspaceKey)
|
|
161
|
+
return React.createElement('div', { key: ws.workspaceKey, className: 'vsm-perf-ws' },
|
|
162
|
+
React.createElement('div', { className: 'vsm-perf-ws-head' },
|
|
163
|
+
React.createElement('span', { className: 'vsm-perf-ws-name', title: ws.workspaceKey }, ws.workspaceKey),
|
|
164
|
+
React.createElement('span', { className: 'vsm-perf-ws-meta' }, rows.length + ' 会话 · ' + formatBytes(ws.totalBytes)),
|
|
165
|
+
),
|
|
166
|
+
React.createElement('div', { className: 'vsm-perf-rows' },
|
|
167
|
+
rows.length ? rows.map((row) => React.createElement(SessionRow, {
|
|
168
|
+
key: keyOf(row.workspaceKey, row.sessionId),
|
|
169
|
+
row,
|
|
170
|
+
checked: !!selected[keyOf(row.workspaceKey, row.sessionId)],
|
|
171
|
+
disabled: row.active || busy === 'move',
|
|
172
|
+
onToggle: () => {
|
|
173
|
+
const k = keyOf(row.workspaceKey, row.sessionId)
|
|
174
|
+
setSelected((old) => { const next = { ...old }; if (next[k]) delete next[k]; else next[k] = true; return next })
|
|
175
|
+
},
|
|
176
|
+
})) : React.createElement('div', { className: 'vsm-mcp-empty' }, '无会话'),
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
}) : null
|
|
180
|
+
|
|
181
|
+
const selCount = Object.keys(selected).length
|
|
182
|
+
const confirmBody = confirm ? React.createElement('div', { className: 'vsm-perf-modal' },
|
|
183
|
+
React.createElement('div', { className: 'vsm-perf-modal-card' },
|
|
184
|
+
React.createElement('h3', null, '确认移出 ' + confirm.items.length + ' 个会话?'),
|
|
185
|
+
React.createElement('p', { className: 'vsm-perf-modal-note' }, '将从 ~/.dsh/sessions 移出到 ~/.dsh/sessions-archive(可逆,重启后不再回放,释放 ' + formatBytes(confirm.reclaimedBytes) + ')。活跃会话已排除。'),
|
|
186
|
+
React.createElement('ul', { className: 'vsm-perf-modal-list' }, confirm.items.slice(0, 20).map((item) => React.createElement('li', { key: item.workspaceKey + item.sessionId }, item.workspaceKey + ' / ' + item.sessionId + '(' + formatBytes(item.bytes) + ')'))),
|
|
187
|
+
confirm.items.length > 20 ? React.createElement('div', { className: 'vsm-perf-modal-more' }, '…等 ' + confirm.items.length + ' 个') : null,
|
|
188
|
+
React.createElement('div', { className: 'vsm-perf-modal-actions' },
|
|
189
|
+
React.createElement('button', { className: 'vsm-primary', onClick: doMove, disabled: busy === 'move' }, busy === 'move' ? '执行中…' : '确认移出'),
|
|
190
|
+
React.createElement('button', { className: 'vsm-secondary', onClick: () => setConfirm(null) }, '取消'),
|
|
191
|
+
),
|
|
192
|
+
),
|
|
193
|
+
) : null
|
|
194
|
+
|
|
195
|
+
const movedBody = moved?.moved?.length ? React.createElement('div', { className: 'vsm-perf-moved' },
|
|
196
|
+
React.createElement('h3', null, '已移出 ' + moved.moved.length + ' 个会话(释放 ' + formatBytes(moved.reclaimedBytes) + ')'),
|
|
197
|
+
React.createElement('div', { className: 'vsm-perf-rows' }, moved.moved.map((item) => React.createElement('div', { key: item.workspaceKey + item.sessionId, className: 'vsm-perf-row' },
|
|
198
|
+
React.createElement('span', { className: 'vsm-perf-cell vsm-perf-id' }, item.sessionId),
|
|
199
|
+
React.createElement('span', { className: 'vsm-perf-cell vsm-perf-size' }, formatBytes(item.bytes)),
|
|
200
|
+
React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: () => doRestore(item), disabled: busy === 'restore' }, '恢复'),
|
|
201
|
+
))),
|
|
202
|
+
moved.failures?.length ? React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, '失败:' + moved.failures.join(';')) : null,
|
|
203
|
+
) : null
|
|
204
|
+
|
|
205
|
+
const configBody = config ? React.createElement('div', { className: 'vsm-perf-config' },
|
|
206
|
+
React.createElement('h3', null, 'DSH 内置压缩调优'),
|
|
207
|
+
React.createElement('p', null, '调低 compaction-basic / tool-result-pruner 触发阈值,让长循环会话更早压缩,降低模型可见上下文压力(减少 token 与防 context overflow)。'),
|
|
208
|
+
React.createElement('p', { className: 'vsm-perf-note' }, '注意:压缩只追加 compaction 事件,不重写已持久化会话日志(append-only),因此不会缩小已存在的会话文件——存量瘦身请用上方「移出到归档」。packChunks 已默认开启,无需配置。'),
|
|
209
|
+
React.createElement('div', { className: 'vsm-perf-config-state' },
|
|
210
|
+
React.createElement('span', { className: 'vsm-perf-cell' }, '状态:' + (config.applied ? '已应用' : '未应用')),
|
|
211
|
+
config.applied ? React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: undoConfig, disabled: busy === 'undo' }, '撤销') : React.createElement('button', { className: 'vsm-primary vsm-small', onClick: applyConfig, disabled: busy === 'apply' }, '应用建议配置(需重启)'),
|
|
212
|
+
),
|
|
213
|
+
React.createElement('pre', { className: 'vsm-perf-block' }, config.block),
|
|
214
|
+
) : null
|
|
215
|
+
|
|
216
|
+
const guideBody = React.createElement('div', { className: 'vsm-perf-guide' },
|
|
217
|
+
React.createElement('h3', null, '侧车摘要(agent/脚本取关键字段,不整份读 sidecar)'),
|
|
218
|
+
React.createElement('p', null, '工作区根目录的 .dsh-edit-review.json(可能数 MB)只应通过摘要接口读取,禁止 agent 整份 read/write 进对话:'),
|
|
219
|
+
React.createElement('pre', { className: 'vsm-perf-block' },
|
|
220
|
+
"curl -s -X POST http://127.0.0.1:3080/edrv/rpc -H 'content-type: application/json' -d '{\"method\":\"edrv.perf.sidecarSummary\",\"args\":{}}'\n# 差异记录列表(含每文件 pending 摘要):edrv.list\n# 历史归档:edrv.archiveList / edrv.archiveRead"),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return React.createElement('section', { className: 'vsm-perf-page' },
|
|
224
|
+
React.createElement('h2', null, '会话性能'),
|
|
225
|
+
React.createElement('p', null, 'DSH 启动会回放 ~/.dsh/sessions 全部会话(V8 展开约 10×),体积越大内存越吃紧。定期把巨型/旧会话移出到归档可显著降低启动内存。'),
|
|
226
|
+
error ? React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error) : null,
|
|
227
|
+
summaryCards,
|
|
228
|
+
React.createElement('div', { className: 'vsm-perf-toolbar' },
|
|
229
|
+
React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: selectBig }, '圈选 ≥1MB 非活跃'),
|
|
230
|
+
React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: clearSel, disabled: !selCount }, '清空选择'),
|
|
231
|
+
React.createElement('span', { className: 'vsm-perf-selcount' }, '已选 ' + selCount + ' 个'),
|
|
232
|
+
React.createElement('button', { className: 'vsm-primary vsm-small', onClick: planMove, disabled: !selCount || busy === 'plan' }, busy === 'plan' ? '规划中…' : '移出所选'),
|
|
233
|
+
React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: doPurge, disabled: busy === 'purge' }, '清除归档(>30天)'),
|
|
234
|
+
React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: load, disabled: busy === 'load' }, '⟳ 刷新'),
|
|
235
|
+
),
|
|
236
|
+
workspaceBlocks,
|
|
237
|
+
movedBody,
|
|
238
|
+
configBody,
|
|
239
|
+
guideBody,
|
|
240
|
+
confirmBody,
|
|
241
|
+
)
|
|
242
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — PerfSizeBadge:对话 header 会话体积指示器。
|
|
4
|
+
* 5s 轮询当前会话持久化体积(host 仅 stat,近零成本),≥1MB 显示、超阈值变色,
|
|
5
|
+
* 引导"一次任务一个短会话":会话过大时建议 /compact 或新开会话。
|
|
6
|
+
* 作者 ddj 2026-09-02
|
|
7
|
+
*/
|
|
8
|
+
import React from 'react'
|
|
9
|
+
import { rpc } from '../rpc.js'
|
|
10
|
+
|
|
11
|
+
/** 软阈值:≥ 此值琥珀色(默认 2MB)。 */
|
|
12
|
+
const SOFT = 2 * 1024 * 1024
|
|
13
|
+
/** 硬阈值:≥ 此值红色(默认 8MB)。 */
|
|
14
|
+
const HARD = 8 * 1024 * 1024
|
|
15
|
+
/** 显示下限:≥ 1MB 才显示(太小无提示价值)。 */
|
|
16
|
+
const SHOW_MIN = 1024 * 1024
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 会话 header 体积徽章(挂 conversation.session.header.utilities)。
|
|
20
|
+
* @author ddj 2026年09月02号
|
|
21
|
+
* @param props { sessionId, sessions }(sessions 供 cwd 解析)
|
|
22
|
+
*/
|
|
23
|
+
export function PerfSizeBadge(props) {
|
|
24
|
+
const sessionId = props?.sessionId
|
|
25
|
+
const sessions = props?.sessions
|
|
26
|
+
const [size, setSize] = React.useState(null)
|
|
27
|
+
const seq = React.useRef(0)
|
|
28
|
+
|
|
29
|
+
const load = React.useCallback(() => {
|
|
30
|
+
if (!sessionId) return
|
|
31
|
+
const cwd = sessions?.list?.getSnapshot?.()?.byId?.[sessionId]?.cwd
|
|
32
|
+
if (!cwd) return
|
|
33
|
+
const s = ++seq.current
|
|
34
|
+
rpc('edrv.perf.sessionSize', { sessionId, cwd }).then((res) => {
|
|
35
|
+
if (s !== seq.current || !res || !res.ok) return
|
|
36
|
+
setSize(res.exists ? res.bytes : 0)
|
|
37
|
+
}).catch(() => {})
|
|
38
|
+
}, [sessionId, sessions])
|
|
39
|
+
|
|
40
|
+
React.useEffect(() => {
|
|
41
|
+
load()
|
|
42
|
+
const t = setInterval(load, 5000)
|
|
43
|
+
return () => { clearInterval(t) }
|
|
44
|
+
}, [load])
|
|
45
|
+
|
|
46
|
+
if (!size || size < SHOW_MIN) return null
|
|
47
|
+
const label = size >= 1024 * 1024 ? (size / (1024 * 1024)).toFixed(1) + 'MB' : Math.round(size / 1024) + 'KB'
|
|
48
|
+
const color = size >= HARD ? '#dc2626' : size >= SOFT ? '#d97706' : 'var(--dsw-text-3,#6b7280)'
|
|
49
|
+
const tip = '当前会话已持久化 ' + label + '(启动回放约放大 10×,占内存)。过大建议 /compact 或新开会话。'
|
|
50
|
+
return React.createElement('span', {
|
|
51
|
+
style: { display: 'inline-flex', alignItems: 'center', height: 30, padding: '0 8px', fontSize: 11, color, borderRadius: 6, cursor: 'default', whiteSpace: 'nowrap', gap: 4, lineHeight: '30px' },
|
|
52
|
+
title: tip,
|
|
53
|
+
}, '会话 ' + label)
|
|
54
|
+
}
|
package/src/compat.ts
CHANGED
|
@@ -14,6 +14,9 @@ import { loadSettingsDeps } from './fileOpenSettings.js'
|
|
|
14
14
|
import { readDevForm } from './devForm.js'
|
|
15
15
|
import type { CompatAdapter, CompatReport } from './shared/compat.js'
|
|
16
16
|
import type { Ctx } from './store.js'
|
|
17
|
+
import { ROUTE_PREFIX } from './paths.js'
|
|
18
|
+
|
|
19
|
+
export { ROUTE_PREFIX }
|
|
17
20
|
|
|
18
21
|
export const PLUGIN_NAME = 'dsh-vscode-mode'
|
|
19
22
|
export const MCP_PACKAGE = '@deepseek-ai/dsh-mcp-client'
|
|
@@ -21,8 +24,6 @@ export const MCP_PACKAGE = '@deepseek-ai/dsh-mcp-client'
|
|
|
21
24
|
export const PROJECT_ENTRY_PREFIX = 'vsm-mcp.'
|
|
22
25
|
/** 旧版项目 MCP 条目前缀(升级残留识别,不显示为全局 MCP)。 */
|
|
23
26
|
export const LEGACY_PROJECT_PREFIX = 'vsm-mcp:'
|
|
24
|
-
/** webServer 路由前缀(RPC 精确路由 + 静态资源前缀共用)。 */
|
|
25
|
-
export const ROUTE_PREFIX = '/edrv'
|
|
26
27
|
|
|
27
28
|
/** 判断新旧格式项目 MCP 条目 id(旧残留不被视为全局 MCP)。 */
|
|
28
29
|
export function isProjectEntryId(id: string): boolean {
|
package/src/fileOpenSettings.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface SettingsDeps {
|
|
|
24
24
|
z: {
|
|
25
25
|
object: (shape: Record<string, unknown>) => { default: (value: Record<string, unknown>) => unknown }
|
|
26
26
|
string: () => { default: (value: string) => unknown }
|
|
27
|
+
boolean: () => { default: (value: boolean) => unknown }
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
package/src/index.ts
CHANGED
|
@@ -15,7 +15,14 @@ import { installIsolation } from './mcpIsolation.js'
|
|
|
15
15
|
import { dropFileIndex } from './workspace.js'
|
|
16
16
|
import { cwdOf } from './registry.js'
|
|
17
17
|
import { setupOpenSettings } from './fileOpenSettings.js'
|
|
18
|
+
import { disposeIndex } from './treeIndex.js'
|
|
19
|
+
import { sweepTreeCache } from './paths.js'
|
|
18
20
|
import { PLUGIN_NAME, buildReport } from './compat.js'
|
|
21
|
+
import { createLspManager } from './lsp/manager.js'
|
|
22
|
+
import { createLspRpc } from './lsp/rpc.js'
|
|
23
|
+
import { installLspSettingsSection } from './lsp/settings.js'
|
|
24
|
+
import { disposeAllServers } from './lsp/transport.js'
|
|
25
|
+
import type { RpcHandlerMap } from './shared/rpc.js'
|
|
19
26
|
import type { Registry } from './registry.js'
|
|
20
27
|
import type { Ctx } from './store.js'
|
|
21
28
|
|
|
@@ -32,9 +39,15 @@ export function apply(ctx: Ctx, config?: unknown): void {
|
|
|
32
39
|
const registry: Registry = new Map()
|
|
33
40
|
const searcher = newSearcher(ctx)
|
|
34
41
|
const contentSearcher = newContentSearcher(ctx)
|
|
42
|
+
/** LSP 服务器管理器(语言智能:跳转/引用/大纲)。 */
|
|
43
|
+
const lspManager = createLspManager((line) => ctx.logger?.debug?.('[' + name + '] ' + line))
|
|
35
44
|
/** 兼容性警告收集(route 护栏等写入,启动日志一并输出)。 */
|
|
36
45
|
const warnings: string[] = []
|
|
37
46
|
setupOpenSettings(ctx, config, () => {})
|
|
47
|
+
void installLspSettingsSection(ctx, {})
|
|
48
|
+
/** LSP RPC 与会话清理(一次性创建,tracker 状态跨请求保留)。 */
|
|
49
|
+
const lspRpc = createLspRpc({ ctx, pluginConfig: config, manager: lspManager })
|
|
50
|
+
const lspHandlers: Partial<RpcHandlerMap> = lspRpc.handlers
|
|
38
51
|
|
|
39
52
|
ctx.on('tools/result', (exec: unknown, result: unknown) => {
|
|
40
53
|
void captureToolResult(ctx, registry, exec, result)
|
|
@@ -47,14 +60,24 @@ export function apply(ctx: Ctx, config?: unknown): void {
|
|
|
47
60
|
dropFileIndex(cwd)
|
|
48
61
|
searcher.dispose(cwd)
|
|
49
62
|
contentSearcher.dispose(cwd)
|
|
63
|
+
disposeIndex(cwd)
|
|
50
64
|
}
|
|
65
|
+
const sid = (session as { id?: unknown })?.id
|
|
66
|
+
if (typeof sid === 'string') lspRpc.disposeSession(sid)
|
|
51
67
|
})
|
|
52
68
|
|
|
53
|
-
registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher), (warning) => warnings.push(warning))
|
|
69
|
+
registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher, lspHandlers), (warning) => warnings.push(warning))
|
|
54
70
|
installIsolation(ctx)
|
|
71
|
+
// 启动清理缓存目录:非当前 schema / 超保留期 / 未知残留(best-effort,不阻塞装配)
|
|
72
|
+
void sweepTreeCache()
|
|
55
73
|
void logCompatSummary(ctx, warnings)
|
|
74
|
+
// 卸载/重启时强杀 LSP 子进程(防残留)
|
|
75
|
+
ctx.effect(() => () => {
|
|
76
|
+
void lspManager.disposeAll().catch(() => {})
|
|
77
|
+
disposeAllServers()
|
|
78
|
+
})
|
|
56
79
|
|
|
57
|
-
ctx.logger?.info?.('[' + name + '] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP
|
|
80
|
+
ctx.logger?.info?.('[' + name + '] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用,语言服务器 LSP 已接入)')
|
|
58
81
|
}
|
|
59
82
|
|
|
60
83
|
/**
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — LSP 客户端(JSON-RPC over stdio)。
|
|
3
|
+
* 职责:请求/通知/握手/超时/服务器端请求处理/生命周期。transport 无关性:
|
|
4
|
+
* 只依赖 write/onMessage 契约,便于单测时注入假传输。
|
|
5
|
+
* 作者 ddj 2026-08-27
|
|
6
|
+
*/
|
|
7
|
+
import { createFrameParser, encodeMessage, requestMessage, notifyMessage } from './jsonrpc.js'
|
|
8
|
+
import type { LspServerCapabilities } from '../shared/lsp.js'
|
|
9
|
+
|
|
10
|
+
export interface LspClientEvents {
|
|
11
|
+
onReady?: (capabilities: LspServerCapabilities) => void
|
|
12
|
+
onExit?: (code: number | null, signal: string | null) => void
|
|
13
|
+
onLog?: (line: string) => void
|
|
14
|
+
onProgress?: (params: unknown) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_TIMEOUT_MS = 20_000
|
|
18
|
+
|
|
19
|
+
export class LspError extends Error {
|
|
20
|
+
code: number
|
|
21
|
+
constructor(code: number, message: string) {
|
|
22
|
+
super(message)
|
|
23
|
+
this.code = code
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface LspClientTransport {
|
|
28
|
+
write(chunk: Buffer): boolean
|
|
29
|
+
dispose(): void
|
|
30
|
+
readonly alive: boolean
|
|
31
|
+
onMessage: ((message: unknown) => void) | null
|
|
32
|
+
onExit: ((code: number | null, signal: string | null) => void) | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface LspClient {
|
|
36
|
+
request<T>(method: string, params: unknown, timeoutMs?: number): Promise<T>
|
|
37
|
+
notify(method: string, params: unknown): void
|
|
38
|
+
initialize(rootUri: string, workspaceFolders: string[]): Promise<LspServerCapabilities>
|
|
39
|
+
shutdown(): Promise<void>
|
|
40
|
+
readonly alive: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 创建 LSP 客户端。
|
|
45
|
+
* @author ddj 2026年08月27号
|
|
46
|
+
* @param transport 传输句柄(write/onMessage/alive/dispose)
|
|
47
|
+
* @param events 事件回调
|
|
48
|
+
* @returns LSP 客户端
|
|
49
|
+
*/
|
|
50
|
+
export function createLspClient(
|
|
51
|
+
transport: LspClientTransport,
|
|
52
|
+
events: LspClientEvents = {},
|
|
53
|
+
): LspClient {
|
|
54
|
+
let nextId = 1
|
|
55
|
+
let alive = transport.alive
|
|
56
|
+
const pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }>()
|
|
57
|
+
let serverCapabilities: LspServerCapabilities = { definition: false, references: false, documentSymbol: false, workspaceSymbol: false, hover: false }
|
|
58
|
+
|
|
59
|
+
const log = (line: string): void => events.onLog?.(line)
|
|
60
|
+
const parser = createFrameParser()
|
|
61
|
+
|
|
62
|
+
transport.onMessage = (message: unknown): void => {
|
|
63
|
+
const msg = message as { id?: unknown; method?: unknown; params?: unknown; result?: unknown; error?: unknown }
|
|
64
|
+
// 服务器端请求/通知带 method(请求还带 id);响应带 id + result/error、无 method。先判 method 再判 id。
|
|
65
|
+
if (typeof msg.method === 'string') {
|
|
66
|
+
if (msg.method === '$/progress') events.onProgress?.(msg.params)
|
|
67
|
+
void handleServerRequest(msg as { method: string; params?: unknown; id?: unknown })
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
if (typeof msg.id === 'number' || typeof msg.id === 'string') {
|
|
71
|
+
const entry = pending.get(Number(msg.id))
|
|
72
|
+
if (!entry) return
|
|
73
|
+
pending.delete(Number(msg.id))
|
|
74
|
+
clearTimeout(entry.timer)
|
|
75
|
+
if (msg.error) {
|
|
76
|
+
const err = msg.error as { code?: number; message?: string }
|
|
77
|
+
entry.reject(new LspError(err.code ?? -32000, err.message ?? 'LSP error'))
|
|
78
|
+
} else {
|
|
79
|
+
entry.resolve(msg.result)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 服务器端请求:消息/日志类打日志,配置/能力类回 result:null,其余回 method not found。 */
|
|
85
|
+
const handleServerRequest = async (msg: { method: string; params?: unknown; id?: unknown }): Promise<void> => {
|
|
86
|
+
const { method, params, id } = msg
|
|
87
|
+
const respond = (payload: Record<string, unknown>): void => {
|
|
88
|
+
transport.write(encodeMessage(payload))
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
if (method === 'window/logMessage' || method === 'window/showMessage' || method === 'window/showMessageRequest') {
|
|
92
|
+
const p = params as { type?: number; message?: string }
|
|
93
|
+
log('[server ' + (p?.type ?? '') + '] ' + String(p?.message ?? ''))
|
|
94
|
+
if (id !== undefined) respond({ jsonrpc: '2.0', id, result: null })
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
if (method === 'workspace/configuration') {
|
|
98
|
+
const p = params as { items?: unknown[] }
|
|
99
|
+
const items = Array.isArray(p?.items) ? p.items.map(() => null) : []
|
|
100
|
+
if (id !== undefined) respond({ jsonrpc: '2.0', id, result: items })
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
// 客户端能力协商类:标准行为是确认(result:null),否则部分服务器(如 LuaLS)会中断工作区初始化
|
|
104
|
+
if (method === 'window/workDoneProgress/create' || method === 'client/registerCapability' || method === 'workspace/workspaceFolders') {
|
|
105
|
+
if (id !== undefined) respond({ jsonrpc: '2.0', id, result: null })
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
if (id !== undefined) {
|
|
109
|
+
respond({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found: ' + method } })
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
log('server request handler error: ' + String(error))
|
|
113
|
+
if (id !== undefined) {
|
|
114
|
+
respond({ jsonrpc: '2.0', id, error: { code: -32603, message: String(error) } })
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const transportExit = transport.onExit
|
|
120
|
+
transport.onExit = (code, signal): void => {
|
|
121
|
+
alive = false
|
|
122
|
+
for (const entry of pending.values()) {
|
|
123
|
+
clearTimeout(entry.timer)
|
|
124
|
+
entry.reject(new Error('LSP 服务器已退出(' + String(code) + '/' + String(signal) + ')'))
|
|
125
|
+
}
|
|
126
|
+
pending.clear()
|
|
127
|
+
events.onExit?.(code, signal)
|
|
128
|
+
transportExit?.(code, signal)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const client: LspClient = {
|
|
132
|
+
async request<T>(method: string, params: unknown, timeoutMs = DEFAULT_TIMEOUT_MS): Promise<T> {
|
|
133
|
+
if (!alive) throw new Error('LSP 服务器未运行')
|
|
134
|
+
const id = nextId++
|
|
135
|
+
return new Promise<T>((resolve, reject) => {
|
|
136
|
+
const timer = setTimeout(() => {
|
|
137
|
+
pending.delete(id)
|
|
138
|
+
reject(new Error('LSP 请求超时:' + method + '(' + timeoutMs + 'ms)'))
|
|
139
|
+
}, timeoutMs)
|
|
140
|
+
pending.set(id, { resolve: resolve as (v: unknown) => void, reject, timer })
|
|
141
|
+
transport.write(encodeMessage(requestMessage(id, method, params)))
|
|
142
|
+
})
|
|
143
|
+
},
|
|
144
|
+
notify(method: string, params: unknown): void {
|
|
145
|
+
if (!alive) return
|
|
146
|
+
transport.write(encodeMessage(notifyMessage(method, params)))
|
|
147
|
+
},
|
|
148
|
+
async initialize(rootUri: string, workspaceFolders: string[]): Promise<LspServerCapabilities> {
|
|
149
|
+
const init = await client.request<{
|
|
150
|
+
capabilities?: {
|
|
151
|
+
definitionProvider?: unknown
|
|
152
|
+
referencesProvider?: unknown
|
|
153
|
+
documentSymbolProvider?: unknown
|
|
154
|
+
workspaceSymbolProvider?: unknown
|
|
155
|
+
hoverProvider?: unknown
|
|
156
|
+
}
|
|
157
|
+
}>('initialize', {
|
|
158
|
+
processId: process.pid,
|
|
159
|
+
rootUri: rootUri || null,
|
|
160
|
+
rootPath: null,
|
|
161
|
+
workspaceFolders: workspaceFolders.length
|
|
162
|
+
? workspaceFolders.map((uri) => ({ uri, name: uri }))
|
|
163
|
+
: null,
|
|
164
|
+
capabilities: {
|
|
165
|
+
window: { workDoneProgress: true },
|
|
166
|
+
workspace: { configuration: true },
|
|
167
|
+
textDocument: {
|
|
168
|
+
synchronization: { dynamicRegistration: false, willSave: false, didSave: true },
|
|
169
|
+
definition: { dynamicRegistration: false },
|
|
170
|
+
references: { dynamicRegistration: false },
|
|
171
|
+
documentSymbol: { dynamicRegistration: false },
|
|
172
|
+
hover: { dynamicRegistration: false },
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
const caps = init?.capabilities ?? {}
|
|
177
|
+
serverCapabilities = {
|
|
178
|
+
definition: Boolean(caps.definitionProvider),
|
|
179
|
+
references: Boolean(caps.referencesProvider),
|
|
180
|
+
documentSymbol: Boolean(caps.documentSymbolProvider),
|
|
181
|
+
workspaceSymbol: Boolean(caps.workspaceSymbolProvider),
|
|
182
|
+
hover: Boolean(caps.hoverProvider),
|
|
183
|
+
}
|
|
184
|
+
client.notify('initialized', {})
|
|
185
|
+
events.onReady?.(serverCapabilities)
|
|
186
|
+
return serverCapabilities
|
|
187
|
+
},
|
|
188
|
+
async shutdown(): Promise<void> {
|
|
189
|
+
try {
|
|
190
|
+
await client.request<unknown>('shutdown', null, 3000)
|
|
191
|
+
} catch (error) {
|
|
192
|
+
log('shutdown: ' + String(error))
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
client.notify('exit', null)
|
|
196
|
+
} catch (error) {
|
|
197
|
+
log('exit: ' + String(error))
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
get alive() {
|
|
201
|
+
return alive
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
return client
|
|
205
|
+
}
|