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.
- package/README.md +9 -8
- package/lib/client.js +412 -209
- package/lib/client.js.map +1 -1
- package/lib/index.js +1284 -486
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/archiveDb.ts +476 -0
- package/src/capture.ts +8 -1
- package/src/client/diffDockStore.ts +13 -0
- package/src/client/monaco/diffRender.ts +3 -0
- package/src/client/state/regions.ts +21 -1
- package/src/client/styles/editor.css +12 -0
- package/src/client/ui/ConversationDiffDock.ts +2 -2
- package/src/client/ui/DiffLauncher.ts +76 -17
- package/src/client/ui/EditorView.ts +90 -7
- package/src/index.ts +2 -0
- package/src/paths.ts +11 -0
- package/src/perf.ts +17 -24
- package/src/revert.ts +93 -1
- package/src/rpc.ts +241 -67
- package/src/shared/diff.ts +69 -20
- package/src/shared/rpc.ts +8 -1
- package/src/store.ts +154 -55
|
@@ -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
|
-
|
|
61
|
+
/** @author ddj 2026年09月30号 @description 仅加载一个归档摘要页并丢弃失效响应。 */
|
|
62
|
+
const loadArchives = (cursor = null) => {
|
|
28
63
|
if (!sessionId) { setArchiveErr('会话不存在'); return }
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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:
|
|
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(
|
|
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(() =>
|
|
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) =>
|
|
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
|
-
/**
|
|
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,
|
|
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
|
-
//
|
|
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 {
|
|
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
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
|