dsh-vscode-mode 0.1.29 → 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.
@@ -128,3 +128,51 @@
128
128
  .vsm-kb-actionsbar{display:flex;align-items:center;gap:10px;margin-top:12px}
129
129
  .vsm-kb-dirty{flex:1;font-size:12px;color:var(--dsw-alias-state-warn-primary,#e5b93f)}
130
130
  .vsm-kb-message{font-size:12px;color:var(--dsw-alias-brand-primary,#4f8cff);margin-top:8px}
131
+
132
+ /* 会话性能(PerfSettings 子 Tab) */
133
+ .vsm-perf-page{display:flex;flex-direction:column;gap:14px;max-width:920px}
134
+ .vsm-perf-page h2{margin:0;font-size:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}
135
+ .vsm-perf-page p{margin:0;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:13px;line-height:1.5}
136
+ .vsm-perf-cards{display:flex;gap:12px;flex-wrap:wrap}
137
+ .vsm-perf-card{display:flex;flex-direction:column;gap:4px;min-width:120px;padding:12px 16px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;background:var(--dsw-alias-bg-layer-2,transparent)}
138
+ .vsm-perf-card span{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1)}
139
+ .vsm-perf-card b{font-size:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}
140
+ .vsm-perf-card b.big{color:var(--dsw-alias-state-error-primary,#e05252)}
141
+ .vsm-perf-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
142
+ .vsm-perf-selcount{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);margin:0 4px}
143
+ .vsm-perf-ws{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;overflow:hidden}
144
+ .vsm-perf-ws-head{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:10px 14px;background:var(--dsw-alias-bg-layer-1,#262626);border-bottom:1px solid var(--dsw-alias-border-l2,#3a3a3a)}
145
+ .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}
146
+ .vsm-perf-ws-meta{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);flex-shrink:0}
147
+ .vsm-perf-rows{display:flex;flex-direction:column}
148
+ .vsm-perf-row{display:flex;align-items:center;gap:10px;padding:7px 14px;border-bottom:1px solid var(--dsw-alias-border-l1,#333);font-size:12px;color:var(--dsw-alias-label-secondary,#c8c8c8)}
149
+ .vsm-perf-row:last-child{border-bottom:0}
150
+ .vsm-perf-row:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff0d)}
151
+ .vsm-perf-row input[type=checkbox]{accent-color:var(--dsw-alias-brand-primary,#6d55ff);cursor:pointer}
152
+ .vsm-perf-cell{min-width:0}
153
+ .vsm-perf-id{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--ds-font-family-code,ui-monospace,monospace);color:var(--dsw-alias-label-primary,#eee)}
154
+ .vsm-perf-size{width:70px;text-align:right;flex-shrink:0;color:var(--dsw-alias-label-secondary,#c8c8c8)}
155
+ .vsm-perf-size.big{color:var(--dsw-alias-state-error-primary,#e05252);font-weight:600}
156
+ .vsm-perf-mtime{width:44px;flex-shrink:0;color:var(--dsw-alias-label-tertiary,#9aa5b1)}
157
+ .vsm-perf-active{width:44px;flex-shrink:0;font-size:11px;color:var(--dsw-alias-state-warn-primary,#e5b93f);text-align:right}
158
+ .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)}
159
+ .vsm-perf-moved h3{margin:0 0 8px;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6)}
160
+ .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)}
161
+ .vsm-perf-config h3{margin:0 0 8px;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6)}
162
+ .vsm-perf-config-state{display:flex;align-items:center;gap:12px;margin:10px 0}
163
+ .vsm-perf-note{font-size:12px;color:var(--dsw-alias-state-warn-primary,#e5b93f)}
164
+ .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%}
165
+ .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)}
166
+ .vsm-perf-guide h3{margin:0 0 8px;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6)}
167
+ .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}
168
+ .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)}
169
+ .vsm-perf-modal-card h3{margin:0;font-size:16px}
170
+ .vsm-perf-modal-note{font-size:12px}
171
+ .vsm-perf-modal-list{list-style:none;margin:0;padding:0;overflow-y:auto;font-size:12px;color:var(--dsw-alias-label-secondary,#c8c8c8);display:flex;flex-direction:column;gap:4px}
172
+ .vsm-perf-modal-list li{padding:4px 6px;border-radius:6px;background:var(--dsw-alias-bg-layer-1,#262626);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
173
+ .vsm-perf-modal-more{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1)}
174
+ .vsm-perf-modal-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:4px}
175
+ .vsm-secondary{border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;padding:8px 16px;background:none;color:var(--dsw-alias-label-primary,#eee);font-size:13px;cursor:pointer;white-space:nowrap}
176
+ .vsm-secondary:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
177
+ .vsm-secondary:disabled{opacity:.55;cursor:default}
178
+ .vsm-small{padding:4px 10px;font-size:12px}
@@ -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 {
@@ -795,7 +796,9 @@ export function EditorView(props) {
795
796
  fail++
796
797
  }
797
798
  }
798
- setRecords((prev) => Object.assign({}, prev, next))
799
+ // 合并结果无变化(批量全部失败/重复点击)时不 setRecords:避免 records 换引用
800
+ // → pendingRegions 换引用 → diff 全量重渲染(内容相同,纯空转)
801
+ if (Object.keys(next).length) setRecords((prev) => Object.assign({}, prev, next))
799
802
  return { ok, fail }
800
803
  }).catch((e) => {
801
804
  setError('批量处理异常:' + String(e))
@@ -813,20 +816,26 @@ export function EditorView(props) {
813
816
  const itemOf = (r, reject) => ({ callId: r.callId, scope: r.create ? 'call' : 'hunk', hunkIndex: r.idx, decision: reject ? 'rejected' : 'accepted' })
814
817
 
815
818
  const acceptFile = () => {
819
+ if (batchBusyRef.current || !pendingRegions.length) return
820
+ batchBusyRef.current = true
816
821
  actMany(pendingRegions.map((r) => itemOf(r, false))).then(({ ok, fail }) => {
822
+ batchBusyRef.current = false
817
823
  reloadFile(true)
818
824
  emitRefresh()
819
825
  setStatus('已采纳 ' + ok + ' 处差异' + (fail ? ',' + fail + ' 处失败' : ''))
820
826
  if (fail) setError(fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
821
- })
827
+ }).catch(() => { batchBusyRef.current = false })
822
828
  }
823
829
  const undoFile = () => {
830
+ if (batchBusyRef.current || !pendingRegions.length) return
831
+ batchBusyRef.current = true
824
832
  actMany([...pendingRegions].reverse().map((r) => itemOf(r, true))).then(({ ok, fail }) => {
833
+ batchBusyRef.current = false
825
834
  reloadFile(true)
826
835
  emitRefresh()
827
836
  setStatus('已不采纳 ' + ok + ' 处差异' + (fail ? ',' + fail + ' 处失败' : ''))
828
837
  if (fail) setError(fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
829
- })
838
+ }).catch(() => { batchBusyRef.current = false })
830
839
  }
831
840
 
832
841
  // 所有差异文件的待处理 hunk 列表(二级菜单 Keep All / Undo All 用)
@@ -1144,7 +1153,7 @@ export function EditorView(props) {
1144
1153
  // 侧边栏引导条(仅旧页签形态且未装 betterSidebar 时显示;可复制安装命令、可关闭)
1145
1154
  const dismissHint = () => {
1146
1155
  setHintDismissed(true)
1147
- try { localStorage.setItem('edrv.side-hint-dismissed', '1') } catch (e) { /* 忽略 */ }
1156
+ try { localStorage.setItem(CACHE_KEY.sideHint, '1') } catch (e) { /* 忽略 */ }
1148
1157
  }
1149
1158
  const copyInstallCmd = () => {
1150
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 隔离已启用)')