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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/lib/client.js +1439 -46
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +3519 -222
  6. package/lib/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/capture.ts +3 -0
  9. package/src/client/events.ts +11 -0
  10. package/src/client/index.ts +20 -2
  11. package/src/client/monaco/lsp/index.ts +30 -0
  12. package/src/client/monaco/lsp/lspClient.ts +192 -0
  13. package/src/client/monaco/lsp/providers.ts +229 -0
  14. package/src/client/outline/OutlinePanel.tsx +1 -1
  15. package/src/client/outline/lspSource.ts +49 -0
  16. package/src/client/outline/parse.ts +31 -1
  17. package/src/client/outline/types.ts +2 -0
  18. package/src/client/paths.ts +36 -0
  19. package/src/client/sidebar/panels/FileExplorer.ts +95 -30
  20. package/src/client/sidebar/panels/SearchPanel.ts +11 -2
  21. package/src/client/state/explorerCache.ts +3 -1
  22. package/src/client/state/explorerEntriesCache.ts +172 -0
  23. package/src/client/state/viewStateCache.ts +3 -1
  24. package/src/client/styles/editor.css +16 -0
  25. package/src/client/styles/mcp.css +63 -0
  26. package/src/client/ui/EditorView.ts +61 -6
  27. package/src/client/ui/LspSettings.ts +268 -0
  28. package/src/client/ui/McpSettings.ts +5 -1
  29. package/src/client/ui/PerfSettings.ts +242 -0
  30. package/src/client/ui/PerfSizeBadge.ts +54 -0
  31. package/src/compat.ts +3 -2
  32. package/src/fileOpenSettings.ts +1 -0
  33. package/src/index.ts +25 -2
  34. package/src/lsp/client.ts +205 -0
  35. package/src/lsp/config.ts +107 -0
  36. package/src/lsp/extmgr.ts +208 -0
  37. package/src/lsp/jsonrpc.ts +86 -0
  38. package/src/lsp/manager.ts +133 -0
  39. package/src/lsp/providers.ts +235 -0
  40. package/src/lsp/rpc.ts +334 -0
  41. package/src/lsp/server.ts +346 -0
  42. package/src/lsp/settings.ts +44 -0
  43. package/src/lsp/transport.ts +143 -0
  44. package/src/lsp/uri.ts +62 -0
  45. package/src/lsp/zip.ts +82 -0
  46. package/src/paths.ts +284 -0
  47. package/src/perf.ts +340 -0
  48. package/src/perfPatch.ts +76 -0
  49. package/src/routes.ts +7 -12
  50. package/src/rpc.ts +184 -36
  51. package/src/search/content.ts +14 -7
  52. package/src/search/ripgrep.ts +54 -1
  53. package/src/search/types.ts +4 -1
  54. package/src/shared/lsp.ts +138 -0
  55. package/src/shared/rpc.ts +103 -2
  56. package/src/store.ts +2 -2
  57. package/src/treeIndex.ts +397 -0
  58. 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 {
@@ -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
+ }