dsh-vscode-mode 0.1.35 → 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +389 -128
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
- package/src/client/paths.ts +2 -0
- package/src/client/state/workspaceFoldCache.ts +95 -0
- package/src/client/styles/mcp.css +35 -12
- package/src/client/ui/KeybindingsPanel.ts +8 -5
- package/src/client/ui/LspSettings.ts +14 -8
- package/src/client/ui/McpSettings.ts +110 -49
- package/src/client/ui/PerfSettings.ts +145 -34
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import React from 'react'
|
|
12
12
|
import { rpc } from '../rpc.js'
|
|
13
|
+
import { foldLoad, foldSave, normalizeFoldKey } from '../state/workspaceFoldCache.js'
|
|
13
14
|
import '../styles/mcp.css'
|
|
14
15
|
|
|
15
16
|
/** 字节数人类可读。 */
|
|
@@ -23,6 +24,39 @@ function formatBytes(n) {
|
|
|
23
24
|
/** 会话行 key:workspaceKey \u0000 sessionId。 */
|
|
24
25
|
const keyOf = (ws, id) => ws + '\u0000' + id
|
|
25
26
|
|
|
27
|
+
/** 工作区列表每页条数。 */
|
|
28
|
+
const PAGE_SIZE = 7
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 计算分页(纯函数,可单测)。
|
|
32
|
+
* 作者 ddj 2026年09月02号
|
|
33
|
+
* @param total 总条数
|
|
34
|
+
* @param pageSize 每页条数
|
|
35
|
+
* @param page 当前页码(1 起)
|
|
36
|
+
* @returns 归一化后的分页信息(page 被夹到有效范围)
|
|
37
|
+
*/
|
|
38
|
+
export function paginate(total, pageSize, page) {
|
|
39
|
+
const size = Math.max(1, Math.floor(pageSize) || 1)
|
|
40
|
+
const pages = Math.max(1, Math.ceil(total / size))
|
|
41
|
+
const current = Math.min(Math.max(1, Math.floor(page) || 1), pages)
|
|
42
|
+
const start = (current - 1) * size
|
|
43
|
+
return { page: current, pages, pageSize: size, start, end: Math.min(start + size, total), total }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 取当前页切片(纯函数,可单测)。
|
|
48
|
+
* 作者 ddj 2026年09月02号
|
|
49
|
+
* @param items 全量列表
|
|
50
|
+
* @param page 当前页码(1 起)
|
|
51
|
+
* @param pageSize 每页条数
|
|
52
|
+
* @returns 当前页子数组
|
|
53
|
+
*/
|
|
54
|
+
export function pageSlice(items, page, pageSize) {
|
|
55
|
+
const list = items ?? []
|
|
56
|
+
const { start, end } = paginate(list.length, pageSize, page)
|
|
57
|
+
return list.slice(start, end)
|
|
58
|
+
}
|
|
59
|
+
|
|
26
60
|
/** 单行会话。 */
|
|
27
61
|
function SessionRow({ row, checked, disabled, onToggle }) {
|
|
28
62
|
const age = Math.max(0, (Date.now() - row.mtime) / (24 * 3600 * 1000))
|
|
@@ -36,6 +70,56 @@ function SessionRow({ row, checked, disabled, onToggle }) {
|
|
|
36
70
|
)
|
|
37
71
|
}
|
|
38
72
|
|
|
73
|
+
/**
|
|
74
|
+
* 工作区列表分页器:顶部文案 + 上一页/下一页交互。
|
|
75
|
+
* 作者 ddj 2026年09月02号
|
|
76
|
+
* @param {object} info paginate() 的分页信息
|
|
77
|
+
* @param {Function} onPrev 上一页回调
|
|
78
|
+
* @param {Function} onNext 下一页回调
|
|
79
|
+
*/
|
|
80
|
+
function WorkspacePager({ info, onPrev, onNext }) {
|
|
81
|
+
const { page, pages, start, end, total } = info
|
|
82
|
+
return React.createElement('div', { className: 'vsm-perf-pager' },
|
|
83
|
+
React.createElement('span', { className: 'vsm-perf-pager-text' },
|
|
84
|
+
total ? `第 ${start + 1}–${end} 个 / 共 ${total} 个工作区(第 ${page}/${pages} 页)` : '暂无工作区',
|
|
85
|
+
),
|
|
86
|
+
React.createElement('div', { className: 'vsm-perf-pager-actions' },
|
|
87
|
+
React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: onPrev, disabled: page <= 1 }, '‹ 上一页'),
|
|
88
|
+
React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: onNext, disabled: page >= pages }, '下一页 ›'),
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 工作区栏目(可展开/折叠),内含该工作区的会话行。
|
|
95
|
+
* 作者 ddj 2026年09月02号
|
|
96
|
+
* @param {object} ws 工作区汇总(workspaceKey / totalBytes)
|
|
97
|
+
* @param {object[]} rows 该工作区的会话行
|
|
98
|
+
* @param {boolean} expanded 是否展开
|
|
99
|
+
* @param {Function} onToggleFold 展开/折叠切换回调
|
|
100
|
+
* @param {object} selected 已选会话键集合
|
|
101
|
+
* @param {boolean} locked 会话勾选是否禁用(移出执行中)
|
|
102
|
+
* @param {Function} onToggleRow 单行勾选回调
|
|
103
|
+
*/
|
|
104
|
+
function WorkspaceBlock({ ws, rows, expanded, onToggleFold, selected, locked, onToggleRow }) {
|
|
105
|
+
const head = React.createElement('div', { className: 'vsm-perf-ws-head', role: 'button', tabIndex: 0, 'aria-expanded': expanded, onClick: onToggleFold, onKeyDown: (event) => {
|
|
106
|
+
if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); onToggleFold() }
|
|
107
|
+
} },
|
|
108
|
+
React.createElement('span', { className: 'vsm-perf-ws-caret ' + (expanded ? 'open' : '') }, '▸'),
|
|
109
|
+
React.createElement('span', { className: 'vsm-perf-ws-name', title: ws.workspaceKey }, ws.workspaceKey),
|
|
110
|
+
React.createElement('span', { className: 'vsm-perf-ws-meta' }, rows.length + ' 会话 · ' + formatBytes(ws.totalBytes)),
|
|
111
|
+
)
|
|
112
|
+
if (!expanded) return React.createElement('div', { className: 'vsm-perf-ws' }, head)
|
|
113
|
+
const body = rows.length ? rows.map((row) => React.createElement(SessionRow, {
|
|
114
|
+
key: keyOf(row.workspaceKey, row.sessionId),
|
|
115
|
+
row,
|
|
116
|
+
checked: !!selected[keyOf(row.workspaceKey, row.sessionId)],
|
|
117
|
+
disabled: row.active || locked,
|
|
118
|
+
onToggle: () => onToggleRow(keyOf(row.workspaceKey, row.sessionId)),
|
|
119
|
+
})) : React.createElement('div', { className: 'vsm-mcp-empty' }, '无会话')
|
|
120
|
+
return React.createElement('div', { className: 'vsm-perf-ws expanded' }, head, React.createElement('div', { className: 'vsm-perf-rows' }, body))
|
|
121
|
+
}
|
|
122
|
+
|
|
39
123
|
/**
|
|
40
124
|
* 会话卫生 + 压缩调优设置子页。
|
|
41
125
|
* @author ddj 2026年09月02号
|
|
@@ -48,6 +132,23 @@ export function PerfSettings() {
|
|
|
48
132
|
const [confirm, setConfirm] = React.useState(null)
|
|
49
133
|
const [moved, setMoved] = React.useState(null)
|
|
50
134
|
const [config, setConfig] = React.useState(null)
|
|
135
|
+
const [expanded, setExpanded] = React.useState(() => foldLoad())
|
|
136
|
+
const [page, setPage] = React.useState(1)
|
|
137
|
+
|
|
138
|
+
React.useEffect(() => { foldSave(expanded) }, [expanded])
|
|
139
|
+
|
|
140
|
+
/** 数据变化后把越界页码夹回有效范围。 */
|
|
141
|
+
const workspaces = inv?.workspaces ?? []
|
|
142
|
+
const pageInfo = paginate(workspaces.length, PAGE_SIZE, page)
|
|
143
|
+
React.useEffect(() => {
|
|
144
|
+
if (page !== pageInfo.page) setPage(pageInfo.page)
|
|
145
|
+
}, [page, pageInfo.page])
|
|
146
|
+
|
|
147
|
+
/** 切换某工作区栏目的展开/折叠(默认全折叠)。 */
|
|
148
|
+
const toggleFold = (key) => setExpanded((old) => old.includes(key) ? old.filter((item) => item !== key) : old.concat(key))
|
|
149
|
+
|
|
150
|
+
/** 切换单会话勾选。 */
|
|
151
|
+
const toggleRow = (k) => setSelected((old) => { const next = { ...old }; if (next[k]) delete next[k]; else next[k] = true; return next })
|
|
51
152
|
|
|
52
153
|
const load = React.useCallback(() => {
|
|
53
154
|
setError('')
|
|
@@ -156,27 +257,25 @@ export function PerfSettings() {
|
|
|
156
257
|
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
258
|
) : null
|
|
158
259
|
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
)
|
|
260
|
+
const visible = pageSlice(workspaces, pageInfo.page, PAGE_SIZE)
|
|
261
|
+
const workspacePager = workspaces.length > PAGE_SIZE ? React.createElement(WorkspacePager, {
|
|
262
|
+
info: pageInfo,
|
|
263
|
+
onPrev: () => setPage((old) => Math.max(1, old - 1)),
|
|
264
|
+
onNext: () => setPage((old) => Math.min(pageInfo.pages, old + 1)),
|
|
179
265
|
}) : null
|
|
266
|
+
const workspaceBlocks = workspaces.length ? React.createElement('div', { className: 'vsm-perf-ws-list' }, visible.map((ws) => {
|
|
267
|
+
const key = normalizeFoldKey(ws.workspaceKey)
|
|
268
|
+
return React.createElement(WorkspaceBlock, {
|
|
269
|
+
key: ws.workspaceKey,
|
|
270
|
+
ws,
|
|
271
|
+
rows: inv.sessions.filter((s) => s.workspaceKey === ws.workspaceKey),
|
|
272
|
+
expanded: expanded.includes(key),
|
|
273
|
+
onToggleFold: () => toggleFold(key),
|
|
274
|
+
selected,
|
|
275
|
+
locked: busy === 'move',
|
|
276
|
+
onToggleRow: toggleRow,
|
|
277
|
+
})
|
|
278
|
+
})) : null
|
|
180
279
|
|
|
181
280
|
const selCount = Object.keys(selected).length
|
|
182
281
|
const confirmBody = confirm ? React.createElement('div', { className: 'vsm-perf-modal' },
|
|
@@ -192,18 +291,21 @@ export function PerfSettings() {
|
|
|
192
291
|
),
|
|
193
292
|
) : null
|
|
194
293
|
|
|
195
|
-
const movedBody = moved?.moved?.length ? React.createElement('
|
|
196
|
-
React.createElement('h3',
|
|
294
|
+
const movedBody = moved?.moved?.length ? React.createElement('section', { className: 'vsm-panel' },
|
|
295
|
+
React.createElement('h3', { className: 'vsm-panel-title' }, '已移出 ' + moved.moved.length + ' 个会话(释放 ' + formatBytes(moved.reclaimedBytes) + ')'),
|
|
296
|
+
React.createElement('div', { className: 'vsm-panel-body' },
|
|
197
297
|
React.createElement('div', { className: 'vsm-perf-rows' }, moved.moved.map((item) => React.createElement('div', { key: item.workspaceKey + item.sessionId, className: 'vsm-perf-row' },
|
|
198
298
|
React.createElement('span', { className: 'vsm-perf-cell vsm-perf-id' }, item.sessionId),
|
|
199
299
|
React.createElement('span', { className: 'vsm-perf-cell vsm-perf-size' }, formatBytes(item.bytes)),
|
|
200
300
|
React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: () => doRestore(item), disabled: busy === 'restore' }, '恢复'),
|
|
201
301
|
))),
|
|
202
302
|
moved.failures?.length ? React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, '失败:' + moved.failures.join(';')) : null,
|
|
303
|
+
),
|
|
203
304
|
) : null
|
|
204
305
|
|
|
205
|
-
const configBody = config ? React.createElement('
|
|
206
|
-
React.createElement('h3',
|
|
306
|
+
const configBody = config ? React.createElement('section', { className: 'vsm-panel' },
|
|
307
|
+
React.createElement('h3', { className: 'vsm-panel-title' }, 'DSH 内置压缩调优'),
|
|
308
|
+
React.createElement('div', { className: 'vsm-panel-body' },
|
|
207
309
|
React.createElement('p', null, '调低 compaction-basic / tool-result-pruner 触发阈值,让长循环会话更早压缩,降低模型可见上下文压力(减少 token 与防 context overflow)。'),
|
|
208
310
|
React.createElement('p', { className: 'vsm-perf-note' }, '注意:压缩只追加 compaction 事件,不重写已持久化会话日志(append-only),因此不会缩小已存在的会话文件——存量瘦身请用上方「移出到归档」。packChunks 已默认开启,无需配置。'),
|
|
209
311
|
React.createElement('div', { className: 'vsm-perf-config-state' },
|
|
@@ -211,13 +313,16 @@ export function PerfSettings() {
|
|
|
211
313
|
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
314
|
),
|
|
213
315
|
React.createElement('pre', { className: 'vsm-perf-block' }, config.block),
|
|
316
|
+
),
|
|
214
317
|
) : null
|
|
215
318
|
|
|
216
|
-
const guideBody = React.createElement('
|
|
217
|
-
React.createElement('h3',
|
|
319
|
+
const guideBody = React.createElement('section', { className: 'vsm-panel' },
|
|
320
|
+
React.createElement('h3', { className: 'vsm-panel-title' }, '侧车摘要(agent/脚本取关键字段,不整份读 sidecar)'),
|
|
321
|
+
React.createElement('div', { className: 'vsm-panel-body' },
|
|
218
322
|
React.createElement('p', null, '工作区根目录的 .dsh-edit-review.json(可能数 MB)只应通过摘要接口读取,禁止 agent 整份 read/write 进对话:'),
|
|
219
323
|
React.createElement('pre', { className: 'vsm-perf-block' },
|
|
220
324
|
"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"),
|
|
325
|
+
),
|
|
221
326
|
)
|
|
222
327
|
|
|
223
328
|
return React.createElement('section', { className: 'vsm-perf-page' },
|
|
@@ -225,15 +330,21 @@ export function PerfSettings() {
|
|
|
225
330
|
React.createElement('p', null, 'DSH 启动会回放 ~/.dsh/sessions 全部会话(V8 展开约 10×),体积越大内存越吃紧。定期把巨型/旧会话移出到归档可显著降低启动内存。'),
|
|
226
331
|
error ? React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error) : null,
|
|
227
332
|
summaryCards,
|
|
228
|
-
React.createElement('
|
|
229
|
-
React.createElement('
|
|
230
|
-
React.createElement('
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
333
|
+
React.createElement('section', { className: 'vsm-panel' },
|
|
334
|
+
React.createElement('h3', { className: 'vsm-panel-title' }, '会话盘点'),
|
|
335
|
+
React.createElement('div', { className: 'vsm-panel-body' },
|
|
336
|
+
React.createElement('div', { className: 'vsm-perf-toolbar' },
|
|
337
|
+
React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: selectBig }, '圈选 ≥1MB 非活跃'),
|
|
338
|
+
React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: clearSel, disabled: !selCount }, '清空选择'),
|
|
339
|
+
React.createElement('span', { className: 'vsm-perf-selcount' }, '已选 ' + selCount + ' 个'),
|
|
340
|
+
React.createElement('button', { className: 'vsm-primary vsm-small', onClick: planMove, disabled: !selCount || busy === 'plan' }, busy === 'plan' ? '规划中…' : '移出所选'),
|
|
341
|
+
React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: doPurge, disabled: busy === 'purge' }, '清除归档(>30天)'),
|
|
342
|
+
React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: load, disabled: busy === 'load' }, '⟳ 刷新'),
|
|
343
|
+
),
|
|
344
|
+
workspacePager,
|
|
345
|
+
workspaceBlocks,
|
|
346
|
+
),
|
|
235
347
|
),
|
|
236
|
-
workspaceBlocks,
|
|
237
348
|
movedBody,
|
|
238
349
|
configBody,
|
|
239
350
|
guideBody,
|