dsh-vscode-mode 0.14.2 → 0.14.3

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.
@@ -8,6 +8,35 @@ import React from 'react'
8
8
  import { rpc } from '../rpc.js'
9
9
  import { MONO, btn } from './shared.js'
10
10
 
11
+ /**
12
+ * 文件与批次共同标识归档卡片,避免同文件多批次共用展开状态。
13
+ * @author ddj 2026年09月30号
14
+ * @param entry 归档摘要或展开详情
15
+ * @returns 稳定卡片键
16
+ */
17
+ export function archiveKey(entry) {
18
+ return JSON.stringify([entry.path, entry.batch])
19
+ }
20
+
21
+ /**
22
+ * 追加一页摘要并按文件/批次去重,保留已有卡片的顺序。
23
+ * @author ddj 2026年09月30号
24
+ * @param current 已加载页面
25
+ * @param next 下一页
26
+ * @returns 合并后的摘要列表
27
+ */
28
+ export function mergeArchives(current, next) {
29
+ const merged = [...(current ?? [])]
30
+ const keys = new Set(merged.map(archiveKey))
31
+ for (const entry of next) {
32
+ const key = archiveKey(entry)
33
+ if (keys.has(key)) continue
34
+ keys.add(key)
35
+ merged.push(entry)
36
+ }
37
+ return merged
38
+ }
39
+
11
40
  /**
12
41
  * 全局差异面板:待处理文件列表(点击打开并跳转)+ 归档浏览/批次回滚。
13
42
  * 由状态栏差异 chip / header 角标触发。
@@ -22,27 +51,50 @@ export function DiffLauncher(props) {
22
51
  const [archiveDetail, setArchiveDetail] = React.useState(null)
23
52
  const [archivePath, setArchivePath] = React.useState(null)
24
53
  const [archiveErr, setArchiveErr] = React.useState(null)
54
+ const [nextCursor, setNextCursor] = React.useState(null)
55
+ const [moreBusy, setMoreBusy] = React.useState(false)
56
+ const requestRef = React.useRef(0)
57
+ const detailRef = React.useRef(0)
58
+ const moreRef = React.useRef(false)
25
59
  const [error, setError] = React.useState(null)
26
60
 
27
- const loadArchives = () => {
61
+ /** @author ddj 2026年09月30号 @description 仅加载一个归档摘要页并丢弃失效响应。 */
62
+ const loadArchives = (cursor = null) => {
28
63
  if (!sessionId) { setArchiveErr('会话不存在'); return }
29
- rpc('edrv.archiveList', { sessionId }).then((res) => {
30
- if (res && res.ok && Array.isArray(res.entries)) { setArchives(res.entries); setArchiveErr(null) }
31
- else setArchiveErr(res?.error ? String(res.error) : '归档列表读取失败')
32
- }).catch((e) => setArchiveErr('归档异常:' + String(e)))
64
+ if (cursor && moreRef.current) return
65
+ if (cursor) { moreRef.current = true; setMoreBusy(true); setArchiveErr(null) }
66
+ else { requestRef.current++; setArchives(null); setNextCursor(null); setArchiveErr(null) }
67
+ const generation = requestRef.current
68
+ rpc('edrv.archivePage', { sessionId, ...(cursor ? { cursor } : {}) }).then((res) => {
69
+ if (generation !== requestRef.current) return
70
+ if (res && res.ok && Array.isArray(res.entries)) {
71
+ setArchives((current) => cursor ? mergeArchives(current, res.entries) : res.entries)
72
+ setNextCursor(res.nextCursor ?? null)
73
+ setArchiveErr(null)
74
+ } else setArchiveErr(res?.error ? String(res.error) : '归档分页读取失败')
75
+ }).catch((e) => { if (generation === requestRef.current) setArchiveErr('归档异常:' + String(e)) })
76
+ .finally(() => { if (generation === requestRef.current) { moreRef.current = false; setMoreBusy(false) } })
33
77
  }
34
- const loadArchiveDetail = (path) => {
78
+ /** @author ddj 2026年09月30号 @description 仅加载目标文件和批次详情。 */
79
+ const loadArchiveDetail = (path, batch) => {
35
80
  if (!sessionId) return
81
+ const generation = ++detailRef.current
36
82
  setArchiveDetail(null)
37
- rpc('edrv.archiveRead', { sessionId, path }).then((res) => {
83
+ rpc('edrv.archiveRead', { sessionId, path, batch }).then((res) => {
84
+ if (generation !== detailRef.current) return
38
85
  if (res && res.ok && Array.isArray(res.batches)) setArchiveDetail(res.batches)
39
86
  else setArchiveErr(res?.error ? String(res.error) : '归档详情读取失败')
40
- }).catch((e) => setArchiveErr('归档异常:' + String(e)))
87
+ }).catch((e) => { if (generation === detailRef.current) setArchiveErr('归档异常:' + String(e)) })
41
88
  }
42
89
  React.useEffect(() => {
43
- if (tab !== 'archive') return
44
- setArchives(null); setArchiveDetail(null); setArchivePath(null); setArchiveErr(null)
45
- loadArchives()
90
+ requestRef.current++
91
+ detailRef.current++
92
+ moreRef.current = false
93
+ setMoreBusy(false)
94
+ setArchiveDetail(null); setArchivePath(null); setArchiveErr(null)
95
+ if (tab === 'archive') loadArchives()
96
+ else { setArchives(null); setNextCursor(null) }
97
+ return () => { requestRef.current++; detailRef.current++ }
46
98
  }, [tab, sessionId])
47
99
 
48
100
  let body
@@ -54,17 +106,18 @@ export function DiffLauncher(props) {
54
106
  React.createElement('span', { className: 'edrv-launch-cnt' }, '待处理 ' + f.pending + ' 处'),
55
107
  React.createElement('button', { className: 'edrv-btn-mini edrv-btn-keep', onClick: (e) => { e.stopPropagation(); onOpenFile(f.path) } }, '打开'))))
56
108
  } else {
57
- if (archiveErr) body = React.createElement('div', { className: 'edrv-search-empty', style: { color: 'var(--dsw-alias-state-error-primary,#d9534f)' } }, '错误:' + String(archiveErr))
109
+ if (archiveErr && archives === null) body = React.createElement('div', { className: 'edrv-search-empty', style: { color: 'var(--dsw-alias-state-error-primary,#d9534f)' } }, '错误:' + String(archiveErr), React.createElement('button', { onClick: () => loadArchives(), style: btn(false) }, '重试'))
58
110
  else if (archives === null) body = React.createElement('div', { className: 'edrv-search-empty' }, '加载中…')
59
111
  else if (archives.length === 0) body = React.createElement('div', { className: 'edrv-search-empty' }, '暂无归档记录')
60
112
  else {
61
113
  const inner = archives.map((e) => {
62
- const open = archivePath === e.path
114
+ const entryKey = archiveKey(e)
115
+ const open = archivePath === entryKey
63
116
  let detailEl = null
64
117
  if (open) {
65
118
  if (archiveDetail === null) detailEl = React.createElement('div', { style: { padding: 8, fontSize: 11, color: 'var(--dsw-alias-label-tertiary,#888)' } }, '加载中…')
66
119
  else {
67
- const batches = archiveDetail.filter((b) => b.path === e.path)
120
+ const batches = archiveDetail.filter((b) => b.path === e.path && b.batch === e.batch)
68
121
  detailEl = batches.map((b) => (Array.isArray(b.records) ? b.records : []).map((rec) => {
69
122
  const lines = []
70
123
  const hunks = Array.isArray(rec.hunks) ? rec.hunks : []
@@ -90,8 +143,8 @@ export function DiffLauncher(props) {
90
143
  }))
91
144
  }
92
145
  }
93
- return React.createElement('div', { key: e.at + e.path, style: { border: '1px solid var(--dsw-alias-border-l1,#333)', borderRadius: 8, overflow: 'hidden' } },
94
- React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', padding: '6px 8px', background: 'var(--dsw-alias-bg-layer-1,transparent)', cursor: 'pointer', flexWrap: 'wrap' }, onClick: () => { if (open) { setArchivePath(null); setArchiveDetail(null) } else { setArchivePath(e.path); loadArchiveDetail(e.path) } } },
146
+ return React.createElement('div', { key: entryKey, style: { border: '1px solid var(--dsw-alias-border-l1,#333)', borderRadius: 8, overflow: 'hidden' } },
147
+ React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', padding: '6px 8px', background: 'var(--dsw-alias-bg-layer-1,transparent)', cursor: 'pointer', flexWrap: 'wrap' }, onClick: () => { if (open) { detailRef.current++; setArchivePath(null); setArchiveDetail(null) } else { setArchivePath(entryKey); loadArchiveDetail(e.path, e.batch) } } },
95
148
  React.createElement('span', { style: { flex: 1, fontSize: 12, color: 'var(--dsw-alias-label-primary,#ddd)', wordBreak: 'break-all' } }, e.displayPath ?? e.path),
96
149
  (e.batch != null ? React.createElement('span', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary,#888)' } }, '批次 ' + e.batch) : null),
97
150
  (e.reason ? React.createElement('span', { style: { fontSize: 11, color: 'var(--dsw-alias-state-warn-primary,#b7791f)' } }, e.reason) : null),
@@ -110,7 +163,13 @@ export function DiffLauncher(props) {
110
163
  }, style: btn(false) }, '回滚')),
111
164
  detailEl)
112
165
  })
113
- body = React.createElement('div', { className: 'edrv-launch-arch' }, inner)
166
+ body = React.createElement('div', { className: 'edrv-launch-arch' }, inner,
167
+ archiveErr ? React.createElement('div', { style: { color: 'var(--dsw-alias-state-error-primary,#d9534f)' } }, '归档异常:' + archiveErr,
168
+ React.createElement('button', { onClick: () => { if (nextCursor) loadArchives(nextCursor); else loadArchives() }, style: btn(false) }, '重试')) : null,
169
+ nextCursor || moreBusy ? React.createElement('button', {
170
+ className: 'edrv-btn-mini', disabled: moreBusy || !!archiveErr,
171
+ onClick: () => { if (nextCursor) loadArchives(nextCursor) },
172
+ }, moreBusy ? '加载中…' : '加载更多') : null)
114
173
  }
115
174
  }
116
175
 
@@ -11,7 +11,7 @@ import React from 'react'
11
11
  import { useOpenReceipt } from '../useOpenReceipt.js'
12
12
  import { acceptsLoad, receiptErrorFor, receiptPath, receiptReadyFor, receiptTabPath } from '../receiptState.js'
13
13
  import { bindPanelCommand } from '../panelCommands.js'
14
- import { dbg, rpc } from '../rpc.js'
14
+ import { dbg, isDebug, rpc } from '../rpc.js'
15
15
  import { emitFileChanged, emitRefresh } from '../events.js'
16
16
  import { langOf, loadMonaco, snippetLanguageOf } from '../monaco/loader.js'
17
17
  import { clampZoom, clearZoomMem, dataUrlOf, isImagePath, isSvgPath, recallZoom, rememberZoom, zoomKeyOf, zoomStepOf } from '../imagePreview.js'
@@ -44,7 +44,8 @@ import { CommandPalette } from './CommandPalette.js'
44
44
  import { closeCommandPalette } from '../commandPaletteStore.js'
45
45
  import { DiffLauncher } from './DiffLauncher.js'
46
46
  import { SidebarView } from '../sidebar/SidebarView.js'
47
- import { clearDiffDock, publishDiffDock } from '../diffDockStore.js'
47
+ import { clearDiffDock, dockInLayout, publishDiffDock } from '../diffDockStore.js'
48
+ import { DiffBox } from './DiffBox.js'
48
49
  import { displayDiffTotal, editorDockMode } from '../diffDock.js'
49
50
  import { editorHeight } from '../editorLayout.js'
50
51
  import { revealInExplorer as revealPathInExplorer } from '../fileReveal.js'
@@ -479,7 +480,19 @@ export function EditorView(props) {
479
480
  // 当前 tab 是否为可预览的 Markdown,以及是否正处于预览态(预览态替换 Monaco host,只读)
480
481
  const isMdActive = !!active && isMarkdownPath(active)
481
482
  const mdPreviewing = isMdActive && mdPreviewRef.current.has(active)
482
- const regions = React.useMemo(() => diffRegions(currentRecords, contentReady ? content : null).filter((r) => !r.superseded), [currentRecords, content, contentPath, active])
483
+ const regions = React.useMemo(() => {
484
+ // #region debug log
485
+ const trace = isDebug() && contentReady && currentRecords.length > 0
486
+ const start = trace ? performance.now() : 0
487
+ // #endregion
488
+ const next = diffRegions(currentRecords, contentReady ? content : null).filter((r) => !r.superseded)
489
+ // #region debug log
490
+ if (trace) {
491
+ try { dbg(sessionId, `[DEBUG diffRegions] records=${currentRecords.length} bytes=${content.length} regions=${next.length} ms=${(performance.now() - start).toFixed(1)}`) } catch (e) { /* 诊断不可影响渲染 */ }
492
+ }
493
+ // #endregion
494
+ return next
495
+ }, [currentRecords, content, contentPath, active])
483
496
  // useMemo 稳定引用:否则 hover 等重渲染会让 view zone effect 反复重建(- 号闪烁)
484
497
  const pendingRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && !r.stale), [regions])
485
498
  const staleRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && r.stale), [regions])
@@ -802,14 +815,35 @@ export function EditorView(props) {
802
815
 
803
816
  const refreshRecords = (skipStale) => {
804
817
  if (!sessionId) return
818
+ // #region debug log
819
+ const trace = isDebug()
820
+ const start = trace ? performance.now() : 0
821
+ // #endregion
805
822
  rpc('edrv.list', { sessionId, ...(skipStale ? { skipStale: true } : {}) }).then((res) => {
823
+ // #region debug log
824
+ const received = trace ? performance.now() : 0
825
+ // #endregion
806
826
  if (!res || !res.ok || !Array.isArray(res.records)) return
807
827
  const map = {}
808
828
  for (const r of res.records) {
809
829
  if (r.archived === true) continue
810
830
  map[r.callId] = r
811
831
  }
812
- setRecords((prev) => (sameRecords(prev, map) ? prev : map))
832
+ setRecords((prev) => {
833
+ // #region debug log
834
+ const compareStart = trace ? performance.now() : 0
835
+ // #endregion
836
+ const equal = sameRecords(prev, map)
837
+ // #region debug log
838
+ if (trace) {
839
+ try {
840
+ const compareMs = performance.now() - compareStart
841
+ if (compareMs > 20 || received - start > 100) dbg(sessionId, `[DEBUG diffList] count=${res.records.length} skipStale=${skipStale === true} rpcMs=${(received - start).toFixed(1)} compareMs=${compareMs.toFixed(1)} changed=${!equal}`)
842
+ } catch (e) { /* 诊断不可影响轮询 */ }
843
+ }
844
+ // #endregion
845
+ return equal ? prev : map
846
+ })
813
847
  }).catch((e) => setError('list异常:' + String(e)))
814
848
  }
815
849
 
@@ -942,7 +976,15 @@ export function EditorView(props) {
942
976
  return
943
977
  }
944
978
  setLoadStage({ progress: monaco ? 72 : 12, message: '读取文件内容…' })
979
+ // #region debug log
980
+ const readStart = isDebug() ? performance.now() : 0
981
+ // #endregion
945
982
  rpc('edrv.read', { sessionId: sid, path }).then((res) => {
983
+ // #region debug log
984
+ if (readStart) {
985
+ try { dbg(sid, `[DEBUG diffRead] force=${force === true} ok=${res?.ok === true} bytes=${typeof res?.content === 'string' ? res.content.length : 0} rpcMs=${(performance.now() - readStart).toFixed(1)}`) } catch (e) { /* 诊断不可影响文件读取 */ }
986
+ }
987
+ // #endregion
946
988
  if (!loadCurrent(path, seq)) return
947
989
  if (res && res.ok) {
948
990
  mark(res.version, true)
@@ -2704,7 +2746,20 @@ export function EditorView(props) {
2704
2746
  */
2705
2747
  const actHunk = (region, reject, silent) => {
2706
2748
  const method = reject ? 'edrv.reject' : 'edrv.accept'
2749
+ // #region debug log
2750
+ const trace = isDebug()
2751
+ const start = trace ? performance.now() : 0
2752
+ const traceId = trace ? `${Date.now()}-${Math.floor(Math.random() * 1000)}` : ''
2753
+ if (trace) {
2754
+ try { dbg(sessionId, `[DEBUG diffAction] stage=start trace=${traceId} method=${method} scope=${region.create ? 'call' : 'hunk'}`) } catch (e) { /* 诊断不可影响决策 */ }
2755
+ }
2756
+ // #endregion
2707
2757
  return rpc(method, { sessionId, callId: region.callId, scope: region.create ? 'call' : 'hunk', hunkIndex: region.idx }).then((res) => {
2758
+ // #region debug log
2759
+ if (trace) {
2760
+ try { dbg(sessionId, `[DEBUG diffAction] stage=reply trace=${traceId} method=${method} ok=${res?.ok === true} rpcMs=${(performance.now() - start).toFixed(1)}`) } catch (e) { /* 诊断不可影响决策 */ }
2761
+ }
2762
+ // #endregion
2708
2763
  if (res && res.ok) {
2709
2764
  setRecords((prev) => Object.assign({}, prev, { [region.callId]: res.record }))
2710
2765
  if (!silent) { reloadFile(); emitRefresh() }
@@ -2727,7 +2782,21 @@ export function EditorView(props) {
2727
2782
  */
2728
2783
  const actMany = (items) => {
2729
2784
  if (!items.length) return Promise.resolve({ ok: 0, fail: 0, stale: 0 })
2785
+ // #region debug log
2786
+ const trace = isDebug()
2787
+ const start = trace ? performance.now() : 0
2788
+ const rejected = trace ? items.filter((item) => item.decision === 'rejected').length : 0
2789
+ const traceId = trace ? `${Date.now()}-${Math.floor(Math.random() * 1000)}` : ''
2790
+ if (trace) {
2791
+ try { dbg(sessionId, `[DEBUG diffAction] stage=start trace=${traceId} method=decideBatch items=${items.length} rejected=${rejected}`) } catch (e) { /* 诊断不可影响决策 */ }
2792
+ }
2793
+ // #endregion
2730
2794
  return rpc('edrv.decideBatch', { sessionId, items }).then((res) => {
2795
+ // #region debug log
2796
+ if (trace) {
2797
+ try { dbg(sessionId, `[DEBUG diffAction] stage=reply trace=${traceId} method=decideBatch items=${items.length} rpcMs=${(performance.now() - start).toFixed(1)} ok=${res?.ok === true}`) } catch (e) { /* 诊断不可影响决策 */ }
2798
+ }
2799
+ // #endregion
2731
2800
  if (!res || !res.ok || !Array.isArray(res.results)) {
2732
2801
  setError(res?.error ? String(res.error) : '批量处理失败')
2733
2802
  return { ok: 0, fail: items.length, stale: 0 }
@@ -3700,9 +3769,14 @@ export function EditorView(props) {
3700
3769
  React.createElement('button', { className: 'edrv-pill edrv-pill-undo', onClick: () => { dismissHover(); actHunk(hoverAct.region, true) } }, '↩ Undo'))
3701
3770
  : null
3702
3771
 
3703
- /** 组装差异 dock 快照(发布给 conversation.input.dock 的唯一 DiffBox 实例)。 */
3772
+ /**
3773
+ * 组装当前编辑器的差异上下文,供侧栏本地与会话 dock 共用。
3774
+ * @author ddj 2026年09月29号
3775
+ * @returns 含布局及操作回调的差异快照
3776
+ */
3704
3777
  const buildDockSnapshot = () => ({
3705
3778
  mode: editorDockMode(active),
3779
+ layout,
3706
3780
  pendingRegions,
3707
3781
  staleRegions,
3708
3782
  onAct: actHunk,
@@ -3729,9 +3803,10 @@ export function EditorView(props) {
3729
3803
  onOpenNextFile: openNextFile,
3730
3804
  })
3731
3805
 
3806
+ const dockSnapshot = buildDockSnapshot()
3732
3807
  React.useEffect(() => {
3733
3808
  if (!sessionId) return
3734
- publishDiffDock(sessionId, buildDockSnapshot(), dockSourceRef.current)
3809
+ publishDiffDock(sessionId, dockSnapshot, dockSourceRef.current)
3735
3810
  // 侧栏形态同步 Tab 角标计数(仅本形态写入;页签形态清零避免残留)
3736
3811
  if (layout === 'side') setSidePending(sessionId, sum.totalFiles)
3737
3812
  else setSidePending(sessionId, 0)
@@ -3930,12 +4005,20 @@ export function EditorView(props) {
3930
4005
  }, '添加配置…')
3931
4006
  : null
3932
4007
 
3933
- // 主编辑列(侧边栏右侧):pathBar + tabRow + 编辑/差异区(底部整条留给 DSH 对话输入栏)
4008
+ // 侧栏差异栏悬浮在编辑区底部;中央页签仍使用对话 dock。
4009
+ const sideDock = dockInLayout(dockSnapshot, 'side')
4010
+ ? React.createElement('div', {
4011
+ className: 'edrv-side-diff-dock edrv-diff-dock-shell',
4012
+ 'data-edrv-diff-dock-shell': '1',
4013
+ }, React.createElement(DiffBox, { ...dockSnapshot, dock: true }))
4014
+ : null
4015
+ // 主编辑列(侧边栏右侧):pathBar + tabRow + 编辑/差异区。
3934
4016
  const editorArea = React.createElement('div', { className: 'edrv-editor-area' },
3935
4017
  dapBar,
3936
4018
  addCfgBtn,
3937
4019
  body,
3938
4020
  hoverEl,
4021
+ sideDock,
3939
4022
  overlay)
3940
4023
  // 侧边栏引导条(仅旧页签形态且未装 betterSidebar 时显示;可复制安装命令、可关闭)
3941
4024
  const dismissHint = () => {
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /** dsh-vscode-mode Host composition. @author ddj 2026年09月28号 */
2
2
  import { registerRoutes } from './routes.js'
3
3
  import { captureToolResult } from './capture.js'
4
+ import { closeArchiveDbs } from './archiveDb.js'
4
5
  import { handleRpc } from './rpc.js'
5
6
  import { newSearcher } from './search/orchestrator.js'
6
7
  import { newContentSearcher } from './search/content.js'
@@ -83,6 +84,7 @@ function attachRuntime(ctx: Ctx, runtime: Runtime): void {
83
84
  /** @author ddj 2026年09月28号 Release every manager even if another cleanup rejects. */
84
85
  async function cleanup(): Promise<void> {
85
86
  runtime.fileVersions.dispose()
87
+ closeArchiveDbs()
86
88
  disposeAllServers()
87
89
  const tasks = [() => runtime.lspManager.disposeAll(), () => runtime.svnRpc.dispose(), () => runtime.dapRpc.dispose()]
88
90
  await Promise.allSettled(tasks.map((run) => Promise.resolve().then(run)))
package/src/paths.ts CHANGED
@@ -193,6 +193,17 @@ export function debugLogFile(cwd: string, home = dshHome()): string {
193
193
  return join(pluginLogRoot(home), 'debug.' + hashOf(cwd) + '.log')
194
194
  }
195
195
 
196
+ /**
197
+ * 每工作区归档数据库(用户数据,不受缓存清理预算约束)。
198
+ * @author ddj 2026年09月30号
199
+ * @param cwd 工作区绝对路径
200
+ * @param home DSH home
201
+ * @returns 归档数据库绝对路径
202
+ */
203
+ export function archiveDbFile(cwd: string, home = dshHome()): string {
204
+ return join(home, PLUGIN_ID, 'data', 'archive', hashOf(cwd) + '.sqlite')
205
+ }
206
+
196
207
  /** 插件包 assets 目录(import.meta.url 派生;config.imageDir 可覆盖,见 imageDirOf)。 */
197
208
  export function assetsDirOf(moduleUrl: string): string {
198
209
  return join(dirname(fileURLToPath(moduleUrl)), '..', 'assets')
package/src/perf.ts CHANGED
@@ -10,10 +10,9 @@
10
10
  */
11
11
  import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
12
12
  import { dirname, join, resolve, sep } from 'node:path'
13
- import { parseSidecar } from './store.js'
14
- import { recSummary } from './model.js'
13
+ import { activeSummary } from './archiveDb.js'
15
14
  import { log } from './log.js'
16
- import { SIDECAR, SIDECAR_ARCHIVE, dshHome, sessionIdSegment, sessionWorkspaceKey, sessionsRoot } from './paths.js'
15
+ import { SIDECAR, SIDECAR_ARCHIVE, archiveDbFile, dshHome, sessionIdSegment, sessionWorkspaceKey, sessionsRoot } from './paths.js'
17
16
  import type { PerfMoveFailure, PerfMoveItem, PerfSession, PerfTotals, PerfWorkspace, SidecarPerfSummary } from './shared/rpc.js'
18
17
  import type { Ctx } from './store.js'
19
18
 
@@ -370,29 +369,23 @@ export async function sessionSizeOf(home: string, cwd: string, sessionId: string
370
369
  */
371
370
  export async function sidecarSummaryOf(ctx: Ctx, cwd: string | null): Promise<SidecarPerfSummary> {
372
371
  const fs = ctx.get('fs')
373
- const summary: SidecarPerfSummary = { active: 0, pendingByFile: [], archiveBytes: 0 }
372
+ const summary: SidecarPerfSummary = { active: 0, pendingByFile: [], archiveBytes: 0, legacyArchiveBytes: 0, legacyActiveBytes: 0 }
374
373
  if (!fs || !cwd) return summary
374
+ const current = activeSummary(cwd)
375
+ summary.active = current.active
376
+ summary.pendingByFile = current.pendingByFile
375
377
  try {
376
- const text = await fs.readText(await fs.resolve(SIDECAR, { cwd }))
377
- const data = parseSidecar(text)
378
- const recs = data?.workspaces?.[cwd]?.records
379
- if (recs) {
380
- const byFile = new Map<string, number>()
381
- let active = 0
382
- for (const rec of Object.values(recs)) {
383
- if (rec.archived) continue
384
- active += 1
385
- const sm = recSummary(rec)
386
- if (sm.pending > 0 && rec.path) byFile.set(rec.path, (byFile.get(rec.path) ?? 0) + sm.pending)
387
- }
388
- summary.active = active
389
- summary.pendingByFile = [...byFile.entries()].map(([path, pending]) => ({ path, pending })).sort((a, b) => b.pending - a.pending)
390
- }
391
- try {
392
- const info = await fs.stat(await fs.resolve(SIDECAR_ARCHIVE, { cwd }))
393
- summary.archiveBytes = info?.size ?? 0
394
- } catch (error) { /* 归档缺失忽略 */ }
395
- } catch (error) { /* sidecar 不可读返回空摘要 */ }
378
+ const info = await fs.stat(await fs.resolve(SIDECAR_ARCHIVE, { cwd }))
379
+ summary.legacyArchiveBytes = info?.size ?? 0
380
+ } catch { /* 旧归档缺失忽略 */ }
381
+ try {
382
+ const info = await fs.stat(await fs.resolve(SIDECAR, { cwd }))
383
+ summary.legacyActiveBytes = info?.size ?? 0
384
+ } catch { /* 旧活跃文件缺失忽略 */ }
385
+ const dbPath = archiveDbFile(cwd)
386
+ for (const file of [dbPath, dbPath + '-wal', dbPath + '-shm']) {
387
+ try { summary.archiveBytes += (await stat(file)).size } catch { /* 数据库尚未创建 */ }
388
+ }
396
389
  return summary
397
390
  }
398
391
 
package/src/revert.ts CHANGED
@@ -7,6 +7,7 @@ import type { DiffRecord } from './shared/types.js'
7
7
  import type { Ctx, Session } from './store.js'
8
8
  import { policyOf, resolveTarget } from './store.js'
9
9
  import { applyLocations, locateHunks, preciseHunk } from './shared/diff.js'
10
+ import { debugRecord } from './debugLog.js'
10
11
 
11
12
  /** 回滚结果;stale=true 表示该 hunk 的新文本已不在文件中(无可回滚内容,不算失败)。 */
12
13
  export type Result = { ok: true } | { ok: false; error: string; stale?: boolean }
@@ -82,25 +83,116 @@ export async function revertCall(ctx: Ctx, session: Session, record: DiffRecord)
82
83
  }
83
84
 
84
85
  /** 单 hunk 回滚:用 callHunk(edit 单 hunk 精确串)经 fs.editText 反替换。 */
85
- export async function revertHunk(ctx: Ctx, session: Session, record: DiffRecord, idx: number): Promise<Result> {
86
+ export async function revertHunk(ctx: Ctx, session: Session, record: DiffRecord, idx: number, traceId?: string): Promise<Result> {
86
87
  const fs = ctx.get('fs')
87
88
  if (!fs) return { ok: false, error: '回滚不可用:缺少 fs' }
88
89
  const precise = preciseHunk(record, idx)
89
90
  if (!precise) return { ok: false, error: '找不到该差异块' }
91
+ // #region debug log
92
+ const started = traceId ? performance.now() : 0
93
+ let readMs = 0
94
+ let locateMs = 0
95
+ let stage = 'resolve'
96
+ // #endregion
90
97
  try {
91
98
  const target = await resolveTarget(ctx, session, record.path)
99
+ stage = 'stat'
92
100
  const info = await fs.stat(target)
93
101
  if (!info || info.type !== 'file') return { ok: false, error: '回滚失败:文件不存在' }
94
102
  if ((info.size ?? 0) > 8 * 1024 * 1024) return { ok: false, error: '回滚失败:文件过大,无法安全定位' }
103
+ stage = 'read'
95
104
  const content = await fs.readText(target)
105
+ // #region debug log
106
+ if (traceId) readMs = performance.now() - started
107
+ const locateStart = traceId ? performance.now() : 0
108
+ // #endregion
109
+ stage = 'locate'
96
110
  const location = locateHunks(content, [precise])[0]
111
+ // #region debug log
112
+ if (traceId) locateMs = performance.now() - locateStart
113
+ // #endregion
97
114
  // 新文本已不在文件中(被后续修改覆盖/撤销):不采纳的目标已达成,标记 stale 供调用方按"已回滚"记录决策
98
115
  if (!location?.matched) return { ok: false, stale: true, error: '回滚失败:该区域在文件中已不存在(可能已被后续修改覆盖)' }
99
116
  const result = applyLocations(content, [location], true)
117
+ stage = 'write'
100
118
  await fs.writeText(target, result.content, void 0, void 0, policyOf(ctx, session))
101
119
  return { ok: true }
102
120
  } catch (error) {
103
121
  return { ok: false, error: '回滚失败:该区域可能已被后续修改影响(' + String(error) + ')' }
122
+ } finally {
123
+ // #region debug log
124
+ if (traceId) {
125
+ try { debugRecord(ctx, session.header?.cwd ?? '', `[DEBUG diffRevert] trace=${traceId} mode=single stage=${stage} readMs=${readMs.toFixed(1)} locateMs=${locateMs.toFixed(1)} totalMs=${(performance.now() - started).toFixed(1)}`) } catch { /* 诊断不可影响回滚 */ }
126
+ }
127
+ // #endregion
128
+ }
129
+ }
130
+
131
+ /**
132
+ * 同一记录的连续 hunk 在内存里逐项回滚,只对文件读写各一次。
133
+ * 不替代单条回滚;调用方只传确定同路径且非远端的连续项。
134
+ * @author ddj 2026年09月29号
135
+ * @param ctx DSH 上下文
136
+ * @param session 会话
137
+ * @param record 差异记录
138
+ * @param indices 按决策顺序排列的 hunk 索引
139
+ * @param traceId 可选的诊断追踪号,仅开启诊断时传入
140
+ * @returns 每个 hunk 对应的独立执行结果
141
+ */
142
+ export async function revertHunks(ctx: Ctx, session: Session, record: DiffRecord, indices: number[], traceId?: string): Promise<Result[]> {
143
+ const fs = ctx.get('fs')
144
+ if (!fs) return indices.map(() => ({ ok: false, error: '回滚不可用:缺少 fs' }))
145
+ const failAll = (error: string): Result[] => indices.map((idx) => preciseHunk(record, idx)
146
+ ? { ok: false, error } : { ok: false, error: '找不到该差异块' })
147
+ try {
148
+ // #region debug log
149
+ const started = traceId ? performance.now() : 0
150
+ // #endregion
151
+ const target = await resolveTarget(ctx, session, record.path)
152
+ const info = await fs.stat(target)
153
+ if (!info || info.type !== 'file') return failAll('回滚失败:文件不存在')
154
+ if ((info.size ?? 0) > 8 * 1024 * 1024) return failAll('回滚失败:文件过大,无法安全定位')
155
+ let content = await fs.readText(target)
156
+ // #region debug log
157
+ const readMs = traceId ? performance.now() - started : 0
158
+ const locateStart = traceId ? performance.now() : 0
159
+ // #endregion
160
+ let size = info.size ?? Buffer.byteLength(content)
161
+ const results: Result[] = []
162
+ for (const idx of indices) {
163
+ const hunk = preciseHunk(record, idx)
164
+ if (!hunk) { results.push({ ok: false, error: '找不到该差异块' }); continue }
165
+ if (size > 8 * 1024 * 1024) {
166
+ results.push({ ok: false, error: '回滚失败:文件过大,无法安全定位' })
167
+ continue
168
+ }
169
+ const location = locateHunks(content, [hunk])[0]
170
+ if (!location?.matched) {
171
+ results.push({ ok: false, stale: true, error: '回滚失败:该区域在文件中已不存在(可能已被后续修改覆盖)' })
172
+ continue
173
+ }
174
+ size += Buffer.byteLength(hunk.oldText ?? '') - Buffer.byteLength(content.slice(location.start, location.end))
175
+ content = applyLocations(content, [location], true).content
176
+ results.push({ ok: true })
177
+ }
178
+ // #region debug log
179
+ const locateMs = traceId ? performance.now() - locateStart : 0
180
+ const writeStart = traceId ? performance.now() : 0
181
+ // #endregion
182
+ if (results.some((result) => result.ok)) {
183
+ await fs.writeText(target, content, void 0, void 0, policyOf(ctx, session))
184
+ }
185
+ // #region debug log
186
+ try {
187
+ if (traceId) {
188
+ const totalMs = performance.now() - started
189
+ debugRecord(ctx, session.header?.cwd ?? '', `[DEBUG diffRevert] trace=${traceId} mode=batch hunks=${indices.length} bytes=${info.size ?? 0} readMs=${readMs.toFixed(1)} locateMs=${locateMs.toFixed(1)} writeMs=${(performance.now() - writeStart).toFixed(1)} totalMs=${totalMs.toFixed(1)}`)
190
+ }
191
+ } catch { /* 诊断不可影响回滚 */ }
192
+ // #endregion
193
+ return results
194
+ } catch (error) {
195
+ return failAll('回滚失败:该区域可能已被后续修改影响(' + String(error) + ')')
104
196
  }
105
197
  }
106
198