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.
@@ -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 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
- )
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('div', { className: 'vsm-perf-moved' },
196
- React.createElement('h3', null, '已移出 ' + moved.moved.length + ' 个会话(释放 ' + formatBytes(moved.reclaimedBytes) + ')'),
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('div', { className: 'vsm-perf-config' },
206
- React.createElement('h3', null, 'DSH 内置压缩调优'),
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('div', { className: 'vsm-perf-guide' },
217
- React.createElement('h3', null, '侧车摘要(agent/脚本取关键字段,不整份读 sidecar)'),
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('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' }, '⟳ 刷新'),
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,