dsh-vscode-mode 0.1.30 → 0.1.32

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.
@@ -22,6 +22,7 @@ import { revealInExplorer as revealPathInExplorer } from '../fileReveal.js'
22
22
  import { setSidePending, SIDEBAR_INSTALL_CMD } from '../sidebarBridge.js'
23
23
  import { upsertViewState, viewStatesLoad, viewStatesSave } from '../state/viewStateCache.js'
24
24
  import { bindingOf, chordOf, matchEvent, useKeybindingsVersion } from '../keybindings.js'
25
+ import { CACHE_KEY } from '../paths.js'
25
26
 
26
27
  /**
27
28
  * 中央编辑区:文件页签(脏点/关闭/打开路径)+ Ctrl+P 搜索 + Monaco 编辑器 +
@@ -64,7 +65,7 @@ export function EditorView(props) {
64
65
  const [focusRequest, setFocusRequest] = React.useState(0) // 外部差异聚焦请求版本
65
66
  const kbVersion = useKeybindingsVersion() // 快捷键配置版本(tooltip 文案随键位刷新)
66
67
  const [hintDismissed, setHintDismissed] = React.useState(() => {
67
- try { return localStorage.getItem('edrv.side-hint-dismissed') === '1' } catch { return false }
68
+ try { return localStorage.getItem(CACHE_KEY.sideHint) === '1' } catch { return false }
68
69
  }) // 侧边栏引导条是否已关闭
69
70
  const editorRef = React.useRef(null)
70
71
  const viewRootRef = React.useRef(null)
@@ -356,7 +357,7 @@ export function EditorView(props) {
356
357
  bootRef.current = true
357
358
  viewStatesRef.current = viewStatesLoad(sessionId)
358
359
  try {
359
- const raw = localStorage.getItem('edrv.editor.v2.' + String(sessionId))
360
+ const raw = localStorage.getItem(CACHE_KEY.editor + String(sessionId))
360
361
  if (raw) {
361
362
  const saved = JSON.parse(raw)
362
363
  if (Array.isArray(saved.tabs) && saved.tabs.length) {
@@ -369,12 +370,12 @@ export function EditorView(props) {
369
370
 
370
371
  React.useEffect(() => {
371
372
  if (!sessionId) return
372
- try { localStorage.setItem('edrv.editor.v2.' + String(sessionId), JSON.stringify({ tabs: tabs.map((t) => t.path), active })) }
373
+ try { localStorage.setItem(CACHE_KEY.editor + String(sessionId), JSON.stringify({ tabs: tabs.map((t) => t.path), active })) }
373
374
  catch (e) { /* 忽略 */ }
374
375
  }, [tabs, active, sessionId])
375
376
 
376
377
  // 侧边栏状态:恢复(显隐/宽度/激活面板);侧栏形态独立键(默认收起,不共享页签形态偏好)
377
- const sidebarKey = 'edrv.sidebar.' + (layout === 'side' ? 'side.' : '') + String(sessionId)
378
+ const sidebarKey = CACHE_KEY.sidebar + (layout === 'side' ? 'side.' : '') + String(sessionId)
378
379
  React.useEffect(() => {
379
380
  if (!sessionId) return
380
381
  try {
@@ -1152,7 +1153,7 @@ export function EditorView(props) {
1152
1153
  // 侧边栏引导条(仅旧页签形态且未装 betterSidebar 时显示;可复制安装命令、可关闭)
1153
1154
  const dismissHint = () => {
1154
1155
  setHintDismissed(true)
1155
- try { localStorage.setItem('edrv.side-hint-dismissed', '1') } catch (e) { /* 忽略 */ }
1156
+ try { localStorage.setItem(CACHE_KEY.sideHint, '1') } catch (e) { /* 忽略 */ }
1156
1157
  }
1157
1158
  const copyInstallCmd = () => {
1158
1159
  if (navigator.clipboard?.writeText) navigator.clipboard.writeText(SIDEBAR_INSTALL_CMD).catch(() => {})
@@ -12,6 +12,7 @@ import '../styles/mcp.css'
12
12
  import { availableOpeners, AUTO_OPEN_TOOL } from '../fileOpeners.js'
13
13
  import { SettingsContext } from '../settingsContext.js'
14
14
  import { KeybindingsPanel } from './KeybindingsPanel.js'
15
+ import { PerfSettings } from './PerfSettings.js'
15
16
 
16
17
  const EMPTY = { serverName: '', transport: 'stdio', command: '', args: '', cwd: '', url: '', headers: '' }
17
18
 
@@ -321,11 +322,12 @@ export function McpSettings(props) {
321
322
  else if (tab === 'mine') body = React.createElement('div', null, servers.length ? servers.map((s) => React.createElement(ServerCard, { key: s.id, server: s, onRefresh: refreshGlobal, onToggle: toggleGlobal, onRemove: removeGlobal })) : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有配置全局 MCP'))
322
323
  else if (tab === 'projects') body = projectBody
323
324
  else if (tab === 'compat') body = React.createElement(CompatSection, { getSummary: compatSummary })
325
+ else if (tab === 'perf') body = React.createElement(PerfSettings, null)
324
326
  else body = marketBody
325
327
  const form = showForm ? React.createElement(McpForm, { title: '添加全局 MCP', draft, busy, edit, save: saveGlobal, close: () => setShowForm(false) }) : projectForm ? React.createElement(McpForm, { title: '添加项目 MCP · ' + projectForm.title, draft, busy, edit, save: saveProject, close: () => setProjectForm(null) }) : null
326
328
  return React.createElement('section', { className: 'vsm-mcp-page' },
327
329
  React.createElement('header', { className: 'vsm-mcp-header' }, React.createElement('div', null, React.createElement('h2', null, 'VSCodeMode'), React.createElement('p', null, '管理当前 profile 与各项目的 Model Context Protocol 服务。')), React.createElement('button', { className: 'vsm-primary', onClick: () => { setDraft(EMPTY); setShowForm(true) } }, '+ 添加全局 MCP')),
328
- 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 === 'mine' ? 'active' : '', onClick: () => setTab('mine') }, '我的 MCP'), React.createElement('button', { className: tab === 'projects' ? 'active' : '', onClick: () => setTab('projects') }, '项目 MCP'), React.createElement('button', { className: tab === 'market' ? 'active' : '', onClick: () => setTab('market') }, 'MCP 市场'), React.createElement('button', { className: tab === 'compat' ? 'active' : '', onClick: () => setTab('compat') }, '兼容性')),
330
+ React.createElement('nav', { className: 'vsm-mcp-tabs' }, React.createElement('button', { className: tab === 'general' ? 'active' : '', onClick: () => setTab('general') }, '通用'), React.createElement('button', { className: tab === 'keys' ? 'active' : '', onClick: () => setTab('keys') }, '快捷键'), React.createElement('button', { className: tab === 'mine' ? 'active' : '', onClick: () => setTab('mine') }, '我的 MCP'), React.createElement('button', { className: tab === 'projects' ? 'active' : '', onClick: () => setTab('projects') }, '项目 MCP'), React.createElement('button', { className: tab === 'perf' ? 'active' : '', onClick: () => setTab('perf') }, '性能优化'), React.createElement('button', { className: tab === 'market' ? 'active' : '', onClick: () => setTab('market') }, 'MCP 市场'), React.createElement('button', { className: tab === 'compat' ? 'active' : '', onClick: () => setTab('compat') }, '兼容性')),
329
331
  error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
330
332
  body,
331
333
  form,
@@ -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/index.ts CHANGED
@@ -15,6 +15,8 @@ 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'
19
21
  import type { Registry } from './registry.js'
20
22
  import type { Ctx } from './store.js'
@@ -47,11 +49,14 @@ export function apply(ctx: Ctx, config?: unknown): void {
47
49
  dropFileIndex(cwd)
48
50
  searcher.dispose(cwd)
49
51
  contentSearcher.dispose(cwd)
52
+ disposeIndex(cwd)
50
53
  }
51
54
  })
52
55
 
53
56
  registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher), (warning) => warnings.push(warning))
54
57
  installIsolation(ctx)
58
+ // 启动清理缓存目录:非当前 schema / 超保留期 / 未知残留(best-effort,不阻塞装配)
59
+ void sweepTreeCache()
55
60
  void logCompatSummary(ctx, warnings)
56
61
 
57
62
  ctx.logger?.info?.('[' + name + '] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用)')