dsh-vscode-mode 0.19.2 → 0.20.1
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 +26 -15
- package/lib/client.js +4427 -3510
- package/lib/client.js.map +1 -1
- package/lib/index.js +937 -42
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/archiveDb.ts +8 -8
- package/src/capture.ts +2 -2
- package/src/client/monaco/diffRender.ts +2 -2
- package/src/client/reviewBatch.ts +64 -0
- package/src/client/state/diffSummary.ts +30 -0
- package/src/client/state/reviewFeed.ts +64 -0
- package/src/client/styles/editor.css +39 -0
- package/src/client/ui/ConversationDiffDock.ts +9 -10
- package/src/client/ui/DiffBadge.ts +8 -6
- package/src/client/ui/DiffBox.ts +3 -0
- package/src/client/ui/DiffLauncher.ts +21 -5
- package/src/client/ui/EditorView.ts +163 -13
- package/src/client/ui/OfficialDiffBar.tsx +112 -0
- package/src/client/ui/OfficialReview.ts +93 -0
- package/src/client-primitives.d.ts +22 -0
- package/src/index.ts +6 -0
- package/src/model.ts +7 -3
- package/src/officialCapture.ts +203 -0
- package/src/reviewCapture.ts +112 -0
- package/src/reviewDb.ts +285 -0
- package/src/reviewEvidence.ts +70 -0
- package/src/reviewRpc.ts +42 -0
- package/src/reviewUndo.ts +104 -0
- package/src/rpc.ts +14 -18
- package/src/shared/review.ts +95 -0
- package/src/shared/reviewRegions.ts +74 -0
- package/src/shared/rpc.ts +7 -0
- package/src/store.ts +2 -2
|
@@ -22,6 +22,10 @@ import { acceptsLoad, receiptErrorFor, receiptPath, receiptReadyFor, receiptTabP
|
|
|
22
22
|
import { bindPanelCommand } from '../panelCommands.js'
|
|
23
23
|
import { dbg, isDebug, rpc } from '../rpc.js'
|
|
24
24
|
import { refreshFeed, subscribeErrors, subscribeRecords } from '../state/recordsFeed.js'
|
|
25
|
+
import { subscribeReview, refreshReview } from '../state/reviewFeed.js'
|
|
26
|
+
import { runReviewBatch } from '../reviewBatch.js'
|
|
27
|
+
import { reviewRegions } from '../../shared/reviewRegions.js'
|
|
28
|
+
import { diffSummary, officialPath } from '../state/diffSummary.js'
|
|
25
29
|
import { emitFileChanged, emitRefresh } from '../events.js'
|
|
26
30
|
import { langOf, loadMonaco, snippetLanguageOf } from '../monaco/loader.js'
|
|
27
31
|
import { bindEditActions, runEditAction } from '../monaco/editActions.js'
|
|
@@ -256,6 +260,12 @@ export function EditorView(props) {
|
|
|
256
260
|
const [monaco, setMonaco] = React.useState(null)
|
|
257
261
|
const [monacoErr, setMonacoErr] = React.useState(null)
|
|
258
262
|
const [records, setRecords] = React.useState({})
|
|
263
|
+
const [review, setReview] = React.useState({ page: null, error: '' })
|
|
264
|
+
React.useEffect(() => {
|
|
265
|
+
setReview({ page: null, error: '' })
|
|
266
|
+
if (!sessionId) return undefined
|
|
267
|
+
return subscribeReview(sessionId, setReview)
|
|
268
|
+
}, [sessionId])
|
|
259
269
|
const [tabs, setTabs] = React.useState([])
|
|
260
270
|
const [active, setDiskActive] = React.useState(null)
|
|
261
271
|
const [planTabs, setPlanTabs] = React.useState([])
|
|
@@ -434,6 +444,13 @@ export function EditorView(props) {
|
|
|
434
444
|
const hoverEditorRef = React.useRef(false) // 鼠标是否仍在编辑器命中区
|
|
435
445
|
const hoverPanelRef = React.useRef(false) // 鼠标是否已进入 Keep/Undo 浮层
|
|
436
446
|
const batchBusyRef = React.useRef(false) // 批量 Keep All/Undo All 防重入
|
|
447
|
+
const [reviewBatchBusy, setReviewBatchBusy] = React.useState(false)
|
|
448
|
+
const reviewLifeRef = React.useRef(null)
|
|
449
|
+
React.useEffect(() => {
|
|
450
|
+
const token = { live: true }
|
|
451
|
+
reviewLifeRef.current = token
|
|
452
|
+
return () => { token.live = false }
|
|
453
|
+
}, [sessionId])
|
|
437
454
|
const menuHandlersRef = React.useRef(null) // 右键菜单动作的最新闭包(Monaco addAction 空依赖回调读取)
|
|
438
455
|
const svnMenuDisposersRef = React.useRef([]) // Monaco 右键 SVN 组当前 disposables(同步时先注销旧组)
|
|
439
456
|
// 调试(DAP):store 订阅 tick 驱动重渲 + gutter/F9 切换闭包(ensureEditor 空依赖读取)
|
|
@@ -531,11 +548,49 @@ export function EditorView(props) {
|
|
|
531
548
|
const currentRecords = React.useMemo(() => {
|
|
532
549
|
const list = []
|
|
533
550
|
for (const rec of Object.values(records)) {
|
|
534
|
-
if (rec.archived === true) continue
|
|
551
|
+
if (rec.archived === true || officialPath(rec.path, review.page)) continue
|
|
535
552
|
if (sameFile(rec.path, active)) list.push(rec)
|
|
536
553
|
}
|
|
537
554
|
return list
|
|
538
|
-
}, [records, active, cwd])
|
|
555
|
+
}, [records, active, cwd, review.page])
|
|
556
|
+
|
|
557
|
+
const reviewOpsRef = React.useRef(new Map())
|
|
558
|
+
const [officialDetail, setOfficialDetail] = React.useState(null)
|
|
559
|
+
const [officialRecords, setOfficialRecords] = React.useState([])
|
|
560
|
+
const [selectedReview, setSelectedReview] = React.useState(null)
|
|
561
|
+
// Only switching file/session/selection invalidates the visible source immediately.
|
|
562
|
+
// Background metadata refresh must not remove and recreate the current Monaco zones.
|
|
563
|
+
React.useEffect(() => {
|
|
564
|
+
setOfficialDetail(null)
|
|
565
|
+
setOfficialRecords([])
|
|
566
|
+
}, [sessionId, active, selectedReview])
|
|
567
|
+
React.useEffect(() => {
|
|
568
|
+
const controller = new AbortController()
|
|
569
|
+
if (!active || !review.page?.overview?.coveredPaths.some((path) => sameFile(path, active))) {
|
|
570
|
+
setOfficialDetail(null)
|
|
571
|
+
setOfficialRecords([])
|
|
572
|
+
return () => controller.abort()
|
|
573
|
+
}
|
|
574
|
+
/** @private @author ddj 2026年10月27号 Load the active file independently of the global history page. */
|
|
575
|
+
async function loadReview() {
|
|
576
|
+
try {
|
|
577
|
+
// Tabs use workspace-relative paths; the metadata index owns the backend identity.
|
|
578
|
+
const reviewPath = review.page?.overview?.coveredPaths.find((path) => sameFile(path, active)) ?? active
|
|
579
|
+
const page = await rpc('edrv.review.list', { sessionId, path: reviewPath }, controller.signal)
|
|
580
|
+
if (controller.signal.aborted) return
|
|
581
|
+
if (!page.ok) throw new Error(page.error)
|
|
582
|
+
const selected = page.page.items.find((item) => item.id === selectedReview) ?? page.page.items[0]
|
|
583
|
+
if (!selected) { setOfficialRecords([]); setOfficialDetail(null); return }
|
|
584
|
+
const result = await rpc('edrv.review.detail', { sessionId, id: selected.id }, controller.signal)
|
|
585
|
+
if (controller.signal.aborted) return
|
|
586
|
+
if (!result.ok) throw new Error(result.error)
|
|
587
|
+
setOfficialRecords((previous) => JSON.stringify(previous) === JSON.stringify(page.page.items) ? previous : page.page.items)
|
|
588
|
+
setOfficialDetail((previous) => JSON.stringify(previous) === JSON.stringify(result.detail) ? previous : result.detail)
|
|
589
|
+
} catch (cause) { if (!controller.signal.aborted) setError('官方差异读取失败:' + String(cause)) }
|
|
590
|
+
}
|
|
591
|
+
void loadReview()
|
|
592
|
+
return () => controller.abort()
|
|
593
|
+
}, [sessionId, active, review.page, selectedReview, sameFile])
|
|
539
594
|
|
|
540
595
|
const contentReady = content !== null && contentPath === active
|
|
541
596
|
// 当前 tab 是否处于图片预览模式(与 loadContent 的分派条件同源;SVG 文本模式时为 false)
|
|
@@ -550,14 +605,16 @@ export function EditorView(props) {
|
|
|
550
605
|
const trace = isDebug() && contentReady && currentRecords.length > 0
|
|
551
606
|
const start = trace ? performance.now() : 0
|
|
552
607
|
// #endregion
|
|
553
|
-
const next =
|
|
608
|
+
const next = officialDetail
|
|
609
|
+
? reviewRegions(officialDetail.diff, officialDetail.item.id, officialDetail.blocks ?? [], contentReady ? content : null)
|
|
610
|
+
: diffRegions(currentRecords, contentReady ? content : null).filter((r) => !r.superseded)
|
|
554
611
|
// #region debug log
|
|
555
612
|
if (trace) {
|
|
556
613
|
try { dbg(sessionId, `[DEBUG diffRegions] records=${currentRecords.length} bytes=${content.length} regions=${next.length} ms=${(performance.now() - start).toFixed(1)}`) } catch (e) { /* 诊断不可影响渲染 */ }
|
|
557
614
|
}
|
|
558
615
|
// #endregion
|
|
559
616
|
return next
|
|
560
|
-
}, [currentRecords, content, contentPath, active])
|
|
617
|
+
}, [currentRecords, content, contentPath, active, officialDetail])
|
|
561
618
|
// useMemo 稳定引用:否则 hover 等重渲染会让 view zone effect 反复重建(- 号闪烁)
|
|
562
619
|
const pendingRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && !r.stale), [regions])
|
|
563
620
|
const staleRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && r.stale), [regions])
|
|
@@ -575,7 +632,7 @@ export function EditorView(props) {
|
|
|
575
632
|
return map
|
|
576
633
|
}, [pendingRegions])
|
|
577
634
|
lineRegionMapRef.current = lineRegionMap
|
|
578
|
-
const sum = React.useMemo(() =>
|
|
635
|
+
const sum = React.useMemo(() => diffSummary(Object.values(records), review.page), [records, review.page])
|
|
579
636
|
|
|
580
637
|
/** @private @author ddj 2026年10月08号 @param path Disk path. @param select Select tab. @param preview Optional explicit Markdown mode. */
|
|
581
638
|
function addTab(path, select, preview) {
|
|
@@ -2521,7 +2578,7 @@ export function EditorView(props) {
|
|
|
2521
2578
|
if (!line || !map.size) { hoverEditorRef.current = false; hideSoon(); return }
|
|
2522
2579
|
const hit = map.get(line)
|
|
2523
2580
|
if (!hit) { hoverEditorRef.current = false; hideSoon(); return }
|
|
2524
|
-
const key = callIdAttr(hit.callId, hit.idx)
|
|
2581
|
+
const key = hit.sourceKind === 'official' ? hit.recordId + ':' + hit.id : callIdAttr(hit.callId, hit.idx)
|
|
2525
2582
|
const top = Math.max(0, ed.getTopForLineNumber(Math.max(1, hit.start)) - ed.getScrollTop())
|
|
2526
2583
|
const layout = ed.getLayoutInfo()
|
|
2527
2584
|
const minimapLeft = layout.minimap?.minimapLeft || 0
|
|
@@ -3005,6 +3062,7 @@ export function EditorView(props) {
|
|
|
3005
3062
|
* @returns Promise<boolean> 是否成功
|
|
3006
3063
|
*/
|
|
3007
3064
|
const actHunk = (region, reject, silent) => {
|
|
3065
|
+
if (region.sourceKind === 'official') return decideOfficial(reject, region.id)
|
|
3008
3066
|
const method = reject ? 'edrv.reject' : 'edrv.accept'
|
|
3009
3067
|
// #region debug log
|
|
3010
3068
|
const trace = isDebug()
|
|
@@ -3105,7 +3163,38 @@ export function EditorView(props) {
|
|
|
3105
3163
|
*/
|
|
3106
3164
|
const itemOf = (r, reject) => ({ callId: r.callId, scope: r.create ? 'call' : 'hunk', hunkIndex: r.idx, decision: reject ? 'rejected' : 'accepted' })
|
|
3107
3165
|
|
|
3166
|
+
/** @private @author ddj 2026年10月27号 @param reject Undo instead of Keep. @param blockId Optional contiguous block. @returns Persisted decision result; never reloads over a dirty buffer. */
|
|
3167
|
+
async function decideOfficial(reject, blockId) {
|
|
3168
|
+
if (!officialDetail || batchBusyRef.current) return false
|
|
3169
|
+
if (reject && (dirtyRef.current[active] || savingRef.current.has(active) || saveTimerRef.current.isPending())) {
|
|
3170
|
+
setError('存在未保存或正在保存的修改,请等待完成后重试'); return false
|
|
3171
|
+
}
|
|
3172
|
+
batchBusyRef.current = true
|
|
3173
|
+
const source = officialDetail.item
|
|
3174
|
+
const undoPath = active
|
|
3175
|
+
const undoModel = editorRef.current?.getModel?.()
|
|
3176
|
+
const undoVersion = undoModel?.getVersionId?.()
|
|
3177
|
+
const opKey = JSON.stringify([sessionId, source.id, source.seq, source.revision, blockId, reject])
|
|
3178
|
+
let operationId = reviewOpsRef.current.get(opKey)
|
|
3179
|
+
if (!operationId) { operationId = crypto.randomUUID(); reviewOpsRef.current.set(opKey, operationId) }
|
|
3180
|
+
try {
|
|
3181
|
+
const result = await rpc('edrv.review.decide', { sessionId, id: source.id, seq: source.seq,
|
|
3182
|
+
revision: source.revision, operationId, scope: blockId ? 'hunk' : 'file', blockId,
|
|
3183
|
+
decision: reject ? 'rejected' : 'accepted' })
|
|
3184
|
+
if (!result.ok) throw new Error(result.error)
|
|
3185
|
+
reviewOpsRef.current.delete(opKey)
|
|
3186
|
+
if (reject && activeRef.current === undoPath && !dirtyRef.current[undoPath]
|
|
3187
|
+
&& !savingRef.current.has(undoPath) && undoModel?.getVersionId?.() === undoVersion) reloadFile(true)
|
|
3188
|
+
else if (reject) setError('Undo 已完成,编辑器在等待期间变化;未覆盖当前缓冲区,请核对磁盘后刷新')
|
|
3189
|
+
refreshReview(sessionId)
|
|
3190
|
+
setStatus(reject ? '已 Undo 选中差异' : '已 Keep 选中差异')
|
|
3191
|
+
return true
|
|
3192
|
+
} catch (cause) { setError(String(cause)); return false }
|
|
3193
|
+
finally { batchBusyRef.current = false }
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3108
3196
|
const acceptFile = () => {
|
|
3197
|
+
if (officialDetail) return decideOfficial(false)
|
|
3109
3198
|
if (batchBusyRef.current || !decideRegions.length) return
|
|
3110
3199
|
batchBusyRef.current = true
|
|
3111
3200
|
actMany(decideRegions.map((r) => itemOf(r, false))).then((result) => {
|
|
@@ -3117,6 +3206,7 @@ export function EditorView(props) {
|
|
|
3117
3206
|
}).catch(() => { batchBusyRef.current = false })
|
|
3118
3207
|
}
|
|
3119
3208
|
const undoFile = () => {
|
|
3209
|
+
if (officialDetail) return decideOfficial(true)
|
|
3120
3210
|
if (batchBusyRef.current || !decideRegions.length) return
|
|
3121
3211
|
batchBusyRef.current = true
|
|
3122
3212
|
actMany([...decideRegions].reverse().map((r) => itemOf(r, true))).then((result) => {
|
|
@@ -3132,7 +3222,7 @@ export function EditorView(props) {
|
|
|
3132
3222
|
const allPending = React.useMemo(() => {
|
|
3133
3223
|
const out = []
|
|
3134
3224
|
for (const rec of Object.values(records)) {
|
|
3135
|
-
if (rec.superseded === true) continue
|
|
3225
|
+
if (rec.superseded === true || officialPath(rec.path, review.page)) continue
|
|
3136
3226
|
const perHunk = Array.isArray(rec.decisions?.perHunk) ? rec.decisions.perHunk : []
|
|
3137
3227
|
const hunks = Array.isArray(rec.hunks) ? rec.hunks : []
|
|
3138
3228
|
for (let i = 0; i < hunks.length; i++) {
|
|
@@ -3141,7 +3231,7 @@ export function EditorView(props) {
|
|
|
3141
3231
|
}
|
|
3142
3232
|
}
|
|
3143
3233
|
return out
|
|
3144
|
-
}, [records])
|
|
3234
|
+
}, [records, review.page])
|
|
3145
3235
|
|
|
3146
3236
|
/**
|
|
3147
3237
|
* 批量处理所有差异文件:采纳全部 / 不采纳全部。不采纳按 at 降序(新改先回滚)排序后一次 RPC。
|
|
@@ -3153,7 +3243,7 @@ export function EditorView(props) {
|
|
|
3153
3243
|
batchBusyRef.current = true
|
|
3154
3244
|
const list = reject ? [...allPending].sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : b.idx - a.idx)) : allPending
|
|
3155
3245
|
const paths = [...new Set(list.map((r) => records[r.callId]?.path).filter(Boolean))]
|
|
3156
|
-
actMany(list.map((r) => itemOf(r, reject))).then((result) => {
|
|
3246
|
+
return actMany(list.map((r) => itemOf(r, reject))).then((result) => {
|
|
3157
3247
|
batchBusyRef.current = false
|
|
3158
3248
|
reloadFile(true)
|
|
3159
3249
|
emitRefresh({ scope, paths: paths.length ? paths : undefined, kind: 'structure' })
|
|
@@ -3161,8 +3251,58 @@ export function EditorView(props) {
|
|
|
3161
3251
|
if (result.fail) setError(result.fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
|
|
3162
3252
|
}).catch(() => { batchBusyRef.current = false })
|
|
3163
3253
|
}
|
|
3164
|
-
|
|
3165
|
-
|
|
3254
|
+
/** @private @author ddj 2026年10月27号 @param reject Undo requires every local buffer to be saved. @param token Starting session lifetime. @returns Throws before the next mutation when unsafe. */
|
|
3255
|
+
function guardReviewAll(reject, token) {
|
|
3256
|
+
if (!token?.live) throw new Error('编辑器或会话已切换,批量操作已停止')
|
|
3257
|
+
if (reject && (Object.values(dirtyRef.current).some(Boolean) || savingRef.current.size || saveTimerRef.current.isPending())) {
|
|
3258
|
+
throw new Error('存在未保存或正在保存的文件,未执行下一项 Undo,请保存后重试')
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
/** @private @author ddj 2026年10月27号 @param detail Frozen official revision. @param reject Undo action. @returns Stable retry ID shared with single-file decisions. */
|
|
3263
|
+
function batchOperation(detail, reject) {
|
|
3264
|
+
const source = detail.item
|
|
3265
|
+
const key = JSON.stringify([sessionId, source.id, source.seq, source.revision, undefined, reject])
|
|
3266
|
+
let operationId = reviewOpsRef.current.get(key)
|
|
3267
|
+
if (!operationId) { operationId = crypto.randomUUID(); reviewOpsRef.current.set(key, operationId) }
|
|
3268
|
+
return operationId
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
/** @private @author ddj 2026年10月27号 @param reject Undo or Keep all workspace pending records, including previous rounds. @returns Explicit partial results; legacy proceeds only when official stage fully succeeds. */
|
|
3272
|
+
async function actReviewAll(reject) {
|
|
3273
|
+
if (batchBusyRef.current) return
|
|
3274
|
+
if (!review.page?.overview) { setError('官方待审列表尚未就绪,未执行批量操作'); return }
|
|
3275
|
+
const count = sum.pendingFiles.length
|
|
3276
|
+
if (!count) return
|
|
3277
|
+
if (!window.confirm(`${reject ? 'Undo All' : 'Keep All'}:处理当前工作区全部 ${count} 个待审差异文件(含其他轮次)?${reject ? '\n撤销会改写磁盘;逐项安全校验,遇到冲突停止,已完成项不会自动恢复。' : '\n只标记待审修改为保留,不改写文件。'}`)) return
|
|
3278
|
+
const token = reviewLifeRef.current
|
|
3279
|
+
const undoPath = activeRef.current
|
|
3280
|
+
const undoModel = editorRef.current?.getModel?.()
|
|
3281
|
+
const undoVersion = undoModel?.getVersionId?.()
|
|
3282
|
+
batchBusyRef.current = true; setReviewBatchBusy(true)
|
|
3283
|
+
setStatus(reject ? '正在检查并 Undo 全部待审记录…' : '正在 Keep 全部待审记录…')
|
|
3284
|
+
try {
|
|
3285
|
+
const result = await runReviewBatch({
|
|
3286
|
+
list: async (cursor) => { const res = await rpc('edrv.review.list', { sessionId, cursor }); if (!res.ok) throw new Error(res.error); return res.page },
|
|
3287
|
+
detail: async (id) => { const res = await rpc('edrv.review.detail', { sessionId, id }); if (!res.ok) throw new Error(res.error); return res.detail },
|
|
3288
|
+
decide: (detail, undo, operationId) => rpc('edrv.review.decide', { sessionId, id: detail.item.id, seq: detail.item.seq, revision: detail.item.revision, operationId, scope: 'file', decision: undo ? 'rejected' : 'accepted' }),
|
|
3289
|
+
guard: () => guardReviewAll(reject, token), operation: batchOperation,
|
|
3290
|
+
}, reject)
|
|
3291
|
+
if (!token?.live) return
|
|
3292
|
+
refreshReview(sessionId)
|
|
3293
|
+
if (reject && result.paths.some((path) => sameFile(path, undoPath)) && activeRef.current === undoPath
|
|
3294
|
+
&& !dirtyRef.current[undoPath] && !savingRef.current.has(undoPath) && undoModel?.getVersionId?.() === undoVersion) reloadFile(true)
|
|
3295
|
+
if (result.paths.length) emitRefresh({ scope, paths: result.paths, kind: 'structure' })
|
|
3296
|
+
setStatus(`${reject ? 'Undo All' : 'Keep All'}:官方记录已完成 ${result.completed} / ${result.total}`)
|
|
3297
|
+
if (result.error) { setError(`批量已停止,已完成 ${result.completed} 项,其余未处理。${result.error}`); return }
|
|
3298
|
+
guardReviewAll(reject, token)
|
|
3299
|
+
// Official-owned paths are excluded from allPending; never run legacy writes for official history.
|
|
3300
|
+
if (allPending.length) { batchBusyRef.current = false; await actAllPending(reject) }
|
|
3301
|
+
} catch (cause) { if (token?.live) setError(String(cause)) }
|
|
3302
|
+
finally { batchBusyRef.current = false; if (token?.live) setReviewBatchBusy(false) }
|
|
3303
|
+
}
|
|
3304
|
+
const acceptAllFiles = () => actReviewAll(false)
|
|
3305
|
+
const undoAllFiles = () => actReviewAll(true)
|
|
3166
3306
|
const rollbackFile = () => {
|
|
3167
3307
|
if (!active) return
|
|
3168
3308
|
if (!window.confirm('回滚当前文件到修改前状态?(相关差异将归档)')) return
|
|
@@ -4310,7 +4450,10 @@ export function EditorView(props) {
|
|
|
4310
4450
|
},
|
|
4311
4451
|
},
|
|
4312
4452
|
React.createElement('button', { className: 'edrv-pill edrv-pill-keep', onClick: () => { dismissHover(); actHunk(hoverAct.region, false) } }, '✓ Keep'),
|
|
4313
|
-
React.createElement('button', { className: 'edrv-pill edrv-pill-undo',
|
|
4453
|
+
React.createElement('button', { className: 'edrv-pill edrv-pill-undo',
|
|
4454
|
+
disabled: hoverAct.region.sourceKind === 'official' && !officialDetail?.capability.undo,
|
|
4455
|
+
title: hoverAct.region.sourceKind === 'official' ? officialDetail?.capability.reason : undefined,
|
|
4456
|
+
onClick: () => { dismissHover(); actHunk(hoverAct.region, true) } }, '↩ Undo'))
|
|
4314
4457
|
: null
|
|
4315
4458
|
|
|
4316
4459
|
/**
|
|
@@ -4318,8 +4461,13 @@ export function EditorView(props) {
|
|
|
4318
4461
|
* @author ddj 2026年09月29号
|
|
4319
4462
|
* @returns 含布局及操作回调的差异快照
|
|
4320
4463
|
*/
|
|
4464
|
+
const officialFile = review.page?.overview?.coveredPaths.find((path) => sameFile(path, active))
|
|
4321
4465
|
const buildDockSnapshot = () => ({
|
|
4322
4466
|
mode: editorDockMode(active),
|
|
4467
|
+
official: officialFile ? { sessionId, page: review.page, path: officialFile,
|
|
4468
|
+
pending: sum.files.find((file) => sameFile(file.path, active))?.pending ?? 0,
|
|
4469
|
+
detail: officialDetail, records: officialRecords, onSelect: setSelectedReview } : null,
|
|
4470
|
+
reviewError: review.error || error,
|
|
4323
4471
|
layout,
|
|
4324
4472
|
pendingRegions,
|
|
4325
4473
|
staleRegions,
|
|
@@ -4328,7 +4476,9 @@ export function EditorView(props) {
|
|
|
4328
4476
|
onUndoFile: undoFile,
|
|
4329
4477
|
onAcceptAllFiles: acceptAllFiles,
|
|
4330
4478
|
onUndoAllFiles: undoAllFiles,
|
|
4331
|
-
allPendingCount: allPending.length,
|
|
4479
|
+
allPendingCount: allPending.length + (review.page?.pending ?? 0),
|
|
4480
|
+
reviewBatchBusy,
|
|
4481
|
+
batchUnavailable: !review.page?.overview ? '官方待审列表尚未就绪' : undefined,
|
|
4332
4482
|
onRollback: rollbackFile,
|
|
4333
4483
|
onJump: jumpTo,
|
|
4334
4484
|
otherFiles,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Button, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
3
|
+
import type { ReviewDetail, ReviewItem, ReviewPage } from '../../shared/review.js'
|
|
4
|
+
import { ReviewLedger } from './OfficialReview.js'
|
|
5
|
+
|
|
6
|
+
export interface OfficialBarProps {
|
|
7
|
+
official: { sessionId: string; path: string; pending: number; detail?: ReviewDetail; records?: ReviewItem[]; page: ReviewPage; onSelect: (id: string) => void }
|
|
8
|
+
activePath: string
|
|
9
|
+
pendingRegions?: unknown[]
|
|
10
|
+
staleRegions?: unknown[]
|
|
11
|
+
diffIdx?: number
|
|
12
|
+
fileTotal?: number
|
|
13
|
+
fileIdx?: number
|
|
14
|
+
allPendingCount?: number
|
|
15
|
+
reviewBatchBusy?: boolean
|
|
16
|
+
batchUnavailable?: string
|
|
17
|
+
onAcceptAllFiles?: () => void
|
|
18
|
+
onUndoAllFiles?: () => void
|
|
19
|
+
onPrevFile: () => void
|
|
20
|
+
onNextFile: () => void
|
|
21
|
+
onPrevDiff: () => void
|
|
22
|
+
onNextDiff: () => void
|
|
23
|
+
onAcceptFile: () => void
|
|
24
|
+
onUndoFile: () => void
|
|
25
|
+
onOpenLauncher: (tab: string) => void
|
|
26
|
+
reviewError?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Mark = 'left' | 'right' | 'up' | 'down' | 'check' | 'undo' | 'more' | 'file'
|
|
30
|
+
const marks: Record<Mark, string> = {
|
|
31
|
+
left: 'm10 4-5 4 5 4', right: 'm6 4 5 4-5 4', up: 'm4 10 4-5 4 5', down: 'm4 6 4 5 4-5',
|
|
32
|
+
check: 'm3 8 3 3 7-7', undo: 'M5 3 2 6l3 3M2 6h7a4 4 0 0 1 0 8',
|
|
33
|
+
more: 'M3 8h.01M8 8h.01M13 8h.01', file: 'M9 2H4v12h8V5L9 2Zm0 0v4h3M6 9h4M6 11h3',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @private @author ddj 2026年10月27号 @param mark Toolbar action. @returns Decorative consistent-stroke icon; labels live on buttons. */
|
|
37
|
+
function barIcon(mark: Mark): React.ReactElement {
|
|
38
|
+
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d={marks[mark]} /></svg>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @private @author ddj 2026年10月27号 @param props Global workspace counts and bulk handlers. @returns Persistent global review controls, distinct from current-file actions. */
|
|
42
|
+
function ReviewAllRow(props: OfficialBarProps): React.ReactElement {
|
|
43
|
+
const total = props.fileTotal ?? 0
|
|
44
|
+
const current = total && (props.fileIdx ?? -1) >= 0 ? (props.fileIdx ?? 0) + 1 : 0
|
|
45
|
+
const disabled = props.reviewBatchBusy || !!props.batchUnavailable || !props.allPendingCount
|
|
46
|
+
return <div className="edrv-review-global" role="group" aria-label="全部待审文件">
|
|
47
|
+
<Button size="sm" variant="ghost" className="edrv-review-total" onClick={() => props.onOpenLauncher('pending')} title="打开当前工作区全部待审差异文件">差异文件 <strong>{current} / {total}</strong></Button>
|
|
48
|
+
<span className="edrv-review-all-actions">
|
|
49
|
+
<Button size="sm" variant="ghost" onClick={props.onAcceptAllFiles} disabled={disabled || !props.onAcceptAllFiles} title={props.batchUnavailable || 'Keep 当前工作区全部待审文件(含其他轮次),不改写磁盘'}>Keep All</Button>
|
|
50
|
+
<Button size="sm" variant="ghost" className="edrv-review-undo" onClick={props.onUndoAllFiles} disabled={disabled || !props.onUndoAllFiles} title={props.batchUnavailable || '检查全部待审记录后逐项 Undo;缺少证据或遇到冲突停止,不强制覆盖'}>Undo All</Button>
|
|
51
|
+
</span>
|
|
52
|
+
</div>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @public @author ddj 2026年10月27号 @param props Existing review callbacks and immutable record metadata. @returns Responsive review toolbar; no disk or decision side effects until an explicit action. */
|
|
56
|
+
export function OfficialDiffBar(props: OfficialBarProps): React.ReactElement {
|
|
57
|
+
const { official, pendingRegions = [], staleRegions = [], diffIdx = 0 } = props
|
|
58
|
+
const [expanded, setExpanded] = React.useState(false)
|
|
59
|
+
const [menu, setMenu] = React.useState<'records' | 'more' | null>(null)
|
|
60
|
+
const name = String(props.activePath || '').split(/[\\/]/).pop() || '当前文件'
|
|
61
|
+
const detail = official.detail
|
|
62
|
+
const records = official.records ?? []
|
|
63
|
+
const count = pendingRegions.length
|
|
64
|
+
const position = count ? `${Math.min(diffIdx + 1, count)} / ${count}` : '—'
|
|
65
|
+
|
|
66
|
+
/** @private @author ddj 2026年10月27号 @returns Close current popup without changing selection. */
|
|
67
|
+
function closeMenu(): void { setMenu(null) }
|
|
68
|
+
/** @private @author ddj 2026年10月27号 @param id Immutable record ID. @returns Select record and dismiss popup. */
|
|
69
|
+
function selectRecord(id: string): void { official.onSelect(id); closeMenu() }
|
|
70
|
+
/** @private @author ddj 2026年10月27号 @param tab Existing launcher tab. @returns Open existing overview, never create another review entry. */
|
|
71
|
+
function openOverview(tab: string): void { props.onOpenLauncher(tab); closeMenu() }
|
|
72
|
+
/** @private @author ddj 2026年10月27号 @param mark Navigation icon. @param label Accessible action. @param action Existing callback. @param disabled Whether navigation is unavailable. @returns Compact keyboard-focusable navigation control. */
|
|
73
|
+
function navButton(mark: Mark, label: string, action: () => void, disabled: boolean): React.ReactElement {
|
|
74
|
+
return <Button size="sm" variant="ghost" className="edrv-review-icon" aria-label={label} title={label} disabled={disabled} onClick={action}>{barIcon(mark)}</Button>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const recordTrigger = <Button size="sm" variant="ghost" className="edrv-review-record" aria-label="选择官方差异记录" aria-haspopup="menu" aria-expanded={menu === 'records'} disabled={!records.length} onClick={() => setMenu(menu === 'records' ? null : 'records')}>
|
|
78
|
+
{detail ? `轮次 ${detail.item.turn}` : '选择记录'}{barIcon('down')}
|
|
79
|
+
</Button>
|
|
80
|
+
const moreTrigger = <Button size="sm" variant="ghost" className="edrv-review-icon" aria-label="更多审查操作" title="更多审查操作" aria-haspopup="menu" aria-expanded={menu === 'more'} onClick={() => setMenu(menu === 'more' ? null : 'more')}>{barIcon('more')}</Button>
|
|
81
|
+
|
|
82
|
+
return <div className="edrv-diffbar edrv-diffbar-dock edrv-reviewbar" data-edrv-diffbox="1" data-edrv-diff-source="official" role="region" aria-label="文件差异审查">
|
|
83
|
+
<ReviewAllRow {...props} />
|
|
84
|
+
{props.reviewBatchBusy ? <div className="edrv-review-notice" role="status">正在处理全部待审记录…</div> : null}
|
|
85
|
+
<div className="edrv-review-heading">
|
|
86
|
+
<span className="edrv-review-filemark">{barIcon('file')}</span>
|
|
87
|
+
<span className="edrv-review-filename" title={props.activePath}>{name}</span>
|
|
88
|
+
<span className="edrv-review-pending" title={`${official.pending} 条官方待审记录`}>待审 <strong>{official.pending}</strong></span>
|
|
89
|
+
<Menu open={menu === 'records'} anchor={recordTrigger} items={records.map((item) => ({ id: item.id, label: `轮次 ${item.turn} / 事件 ${item.seq}` }))} selectedId={detail?.item.id} onSelect={selectRecord} onClose={closeMenu} side="top" align="end" portal compact autoFocus />
|
|
90
|
+
</div>
|
|
91
|
+
<div className="edrv-review-tools">
|
|
92
|
+
<div className="edrv-review-nav" role="group" aria-label="文件导航">
|
|
93
|
+
{navButton('left', '上一个差异文件', props.onPrevFile, !props.fileTotal)}
|
|
94
|
+
{navButton('right', '下一个差异文件', props.onNextFile, !props.fileTotal)}
|
|
95
|
+
</div>
|
|
96
|
+
<div className="edrv-review-nav edrv-review-blocknav" role="group" aria-label="差异导航">
|
|
97
|
+
{navButton('up', '上一个差异', props.onPrevDiff, !count)}
|
|
98
|
+
<span className="edrv-review-position" aria-live="polite">{position}</span>
|
|
99
|
+
{navButton('down', '下一个差异', props.onNextDiff, !count)}
|
|
100
|
+
</div>
|
|
101
|
+
<div className="edrv-review-actions">
|
|
102
|
+
<Button size="sm" variant="ghost" className="edrv-review-undo" icon={barIcon('undo')} onClick={props.onUndoFile} disabled={props.reviewBatchBusy || !detail?.capability.undo} title={detail?.capability.reason}>Undo</Button>
|
|
103
|
+
<Button size="sm" variant="primary" className="edrv-review-keep" icon={barIcon('check')} onClick={props.onAcceptFile} disabled={props.reviewBatchBusy || !detail?.capability.keep} title="保留当前记录的未处理修改">Keep</Button>
|
|
104
|
+
<Button size="sm" variant="ghost" className="edrv-review-details" onClick={() => setExpanded(!expanded)} aria-expanded={expanded} aria-label={expanded ? '收起差异' : '展开差异'} title={expanded ? '收起差异' : '展开差异'}>详情{barIcon(expanded ? 'up' : 'down')}</Button>
|
|
105
|
+
<Menu open={menu === 'more'} anchor={moreTrigger} items={[{ id: 'pending', label: '差异总览' }, { id: 'archive', label: '归档 / 历史' }]} onSelect={openOverview} onClose={closeMenu} side="top" align="end" portal compact autoFocus />
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
{detail && staleRegions.length > 0 ? <div className="edrv-review-notice" role="status">{staleRegions.length} 处历史差异无法定位,展开详情查看原始增删。</div> : null}
|
|
109
|
+
{props.reviewError ? <div className="edrv-review-notice edrv-review-error" role="alert">{props.reviewError}</div> : null}
|
|
110
|
+
{expanded ? <div className="edrv-review-body"><ReviewLedger key={`${official.sessionId}:${official.path}`} sessionId={official.sessionId} first={official.page} path={official.path} /></div> : null}
|
|
111
|
+
</div>
|
|
112
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Official immutable patch ledger; current files use the existing editor. @author ddj 2026年10月26号 */
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { rpc } from '../rpc.js'
|
|
4
|
+
import { openEditorView } from '../events.js'
|
|
5
|
+
import { refreshReview } from '../state/reviewFeed.js'
|
|
6
|
+
import type { ReviewDetail, ReviewItem, ReviewPage } from '../../shared/review.js'
|
|
7
|
+
const h = React.createElement
|
|
8
|
+
/** @private @author ddj 2026年10月26号 @param props Latest first-page metadata. @returns Paginated rows independent of expanded historical patch. */
|
|
9
|
+
export function ReviewLedger(props: { sessionId: string; first: ReviewPage | null; history?: boolean; path?: string }) {
|
|
10
|
+
const [page, setPage] = React.useState<ReviewPage | null>(props.first)
|
|
11
|
+
const [history, setHistory] = React.useState(props.history ?? false)
|
|
12
|
+
const [cursor, setCursor] = React.useState(0)
|
|
13
|
+
const [detail, setDetail] = React.useState<ReviewDetail | null>(null)
|
|
14
|
+
const [error, setError] = React.useState('')
|
|
15
|
+
const selected = React.useRef<AbortController | null>(null)
|
|
16
|
+
React.useEffect(() => () => selected.current?.abort(), [])
|
|
17
|
+
React.useEffect(() => {
|
|
18
|
+
const controller = new AbortController()
|
|
19
|
+
if (!history && !props.path && cursor === 0) { setPage(props.first); return () => controller.abort() }
|
|
20
|
+
void rpc('edrv.review.list', { sessionId: props.sessionId, history, cursor, path: props.path }, controller.signal).then((result) => {
|
|
21
|
+
if (controller.signal.aborted) return
|
|
22
|
+
if (result.ok) { setPage(result.page); setError('') }
|
|
23
|
+
else setError(result.error)
|
|
24
|
+
}).catch((cause) => { if (!controller.signal.aborted) setError(String(cause)) })
|
|
25
|
+
return () => controller.abort()
|
|
26
|
+
}, [props.sessionId, props.first, props.path, cursor, history])
|
|
27
|
+
|
|
28
|
+
/** @private @author ddj 2026年10月26号 @param item Frozen version identity; patch is never built from disk. */
|
|
29
|
+
async function select(item: ReviewItem): Promise<void> {
|
|
30
|
+
selected.current?.abort()
|
|
31
|
+
const controller = new AbortController()
|
|
32
|
+
selected.current = controller
|
|
33
|
+
try {
|
|
34
|
+
const result = await rpc('edrv.review.detail', { sessionId: props.sessionId, id: item.id }, controller.signal)
|
|
35
|
+
if (controller.signal.aborted) return
|
|
36
|
+
if (result.ok) { setDetail(result.detail); setError('') }
|
|
37
|
+
else setError(result.error)
|
|
38
|
+
} catch (cause) { if (!controller.signal.aborted) setError(String(cause)) }
|
|
39
|
+
}
|
|
40
|
+
return h('div', { style: { marginTop: 8 } },
|
|
41
|
+
h('label', null, h('input', { type: 'checkbox', checked: history, onChange: () => { setHistory(!history); setCursor(0) } }), '显示已处理及被替换的历史'),
|
|
42
|
+
page?.gaps.map((gap) => h('div', { key: `${gap.sessionId}:${gap.seq}`, role: 'status' }, `轮次 ${gap.turn} 采集不完整:${gap.error}`)),
|
|
43
|
+
error ? h('div', { role: 'alert' }, error) : null,
|
|
44
|
+
!page ? h('p', { role: 'status' }, '正在读取官方审查记录…') : null,
|
|
45
|
+
page && !page.items.length ? h('p', null, history ? '暂无已落盘历史。' : '没有待处理的官方记录;可切换查看历史。') : null,
|
|
46
|
+
h('div', { style: { maxHeight: 210, overflow: 'auto' } }, page?.items.map((item) => h('button', {
|
|
47
|
+
key: item.id, type: 'button', onClick: () => { void select(item) },
|
|
48
|
+
style: { display: 'block', width: '100%', textAlign: 'left', padding: '6px 4px' },
|
|
49
|
+
}, `轮次 ${item.turn} · ${item.display} +${item.added} −${item.deleted} · ${item.decision}${item.current ? '' : '(已被同轮新版本替换)'}`))),
|
|
50
|
+
cursor > 0 ? h('button', { type: 'button', onClick: () => setCursor(Math.max(0, cursor - 50)) }, '上一页') : null,
|
|
51
|
+
page?.nextCursor !== null && page?.nextCursor !== undefined ? h('button', { type: 'button', onClick: () => setCursor(page.nextCursor!) }, '下一页') : null,
|
|
52
|
+
detail ? h(PatchDetail, { key: detail.item.id, sessionId: props.sessionId, detail, onChanged: () => { setDetail(null); refreshReview(props.sessionId) } }) : null)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @private @author ddj 2026年10月26号 @param props Immutable patch and current review revision. @returns Read-only source with explicit capability gates. */
|
|
56
|
+
function PatchDetail(props: { sessionId: string; detail: ReviewDetail; onChanged(): void }) {
|
|
57
|
+
const [error, setError] = React.useState('')
|
|
58
|
+
const [busy, setBusy] = React.useState(false)
|
|
59
|
+
const [operationId] = React.useState(() => crypto.randomUUID())
|
|
60
|
+
const lifetime = React.useRef<AbortController | null>(null)
|
|
61
|
+
React.useEffect(() => {
|
|
62
|
+
lifetime.current = new AbortController()
|
|
63
|
+
return () => lifetime.current?.abort()
|
|
64
|
+
}, [])
|
|
65
|
+
const { item, diff, capability } = props.detail
|
|
66
|
+
/** @private @author ddj 2026年10月26号 Marks reviewed only; never writes disk. Stable operation id survives retries while mounted. */
|
|
67
|
+
async function keep(): Promise<void> {
|
|
68
|
+
setBusy(true)
|
|
69
|
+
try {
|
|
70
|
+
const result = await rpc('edrv.review.decide', { sessionId: props.sessionId, id: item.id, seq: item.seq, revision: item.revision,
|
|
71
|
+
operationId, decision: 'accepted' }, lifetime.current?.signal)
|
|
72
|
+
if (lifetime.current?.signal.aborted) return
|
|
73
|
+
if (result.ok) props.onChanged()
|
|
74
|
+
else setError(result.error)
|
|
75
|
+
} catch (cause) { if (!lifetime.current?.signal.aborted) setError(String(cause)) }
|
|
76
|
+
finally { if (!lifetime.current?.signal.aborted) setBusy(false) }
|
|
77
|
+
}
|
|
78
|
+
return h('article', { style: { marginTop: 10, borderTop: '1px solid var(--border-color, #d9dee7)', paddingTop: 8 } },
|
|
79
|
+
h('strong', null, `${item.display} · 轮次 ${item.turn} / 事件 ${item.seq}`),
|
|
80
|
+
h('div', null, `来源会话 ${item.sessionId} · 采集于 ${props.detail.capturedAt}`),
|
|
81
|
+
h('p', null, capability.reason),
|
|
82
|
+
diff.kind === 'text' ? h('div', null,
|
|
83
|
+
h('div', null, `${diff.before ? '原文件存在' : '原文件不存在'} / ${diff.after ? '结果文件存在' : '结果文件不存在'}${diff.coarse ? ' · 粗粒度比较' : ''}`),
|
|
84
|
+
h('pre', { style: { overflow: 'auto', maxHeight: 400, fontSize: 11, lineHeight: 1.6 }, 'aria-label': '官方历史差异(只读)' },
|
|
85
|
+
diff.hunks.map((chunk, index) => h('div', { key: index },
|
|
86
|
+
h('div', null, `@@ -${chunk.oldStart},${chunk.oldLines} +${chunk.newStart},${chunk.newLines} @@`),
|
|
87
|
+
chunk.lines.map((line, position) => h('div', { key: position, style: { background: line.startsWith('+') ? 'light-dark(#e5f5e8, #173522)' : line.startsWith('-') ? 'light-dark(#fce9e9, #452323)' : undefined } }, line)))))
|
|
88
|
+
) : h('p', null, diff.kind === 'binary' ? '二进制文件:官方不提供文本差异。' : '文件超过官方大小限制:没有差异内容。'),
|
|
89
|
+
error ? h('div', { role: 'alert' }, error) : null,
|
|
90
|
+
h('button', { type: 'button', disabled: busy || !capability.keep, onClick: () => { void keep() } }, '标记已审(不改文件)'),
|
|
91
|
+
h('button', { type: 'button', disabled: true, title: capability.reason }, '拒绝写回不可用'),
|
|
92
|
+
h('button', { type: 'button', onClick: () => openEditorView(item.currentPath) }, '编辑当前文件'))
|
|
93
|
+
}
|
|
@@ -7,6 +7,28 @@
|
|
|
7
7
|
* 作者 ddj 2026-09-10
|
|
8
8
|
*/
|
|
9
9
|
declare module '@deepseek-ai/dsh-client-ui-primitives' {
|
|
10
|
+
/** Compact theme-backed button; mirrors official Button.d.ts. @author ddj 2026年10月27号 */
|
|
11
|
+
export const Button: import('react').ComponentType<import('react').ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
12
|
+
variant?: 'primary' | 'ghost' | 'outline' | 'toolbar'
|
|
13
|
+
size?: 'md' | 'sm'
|
|
14
|
+
icon?: import('react').ReactNode
|
|
15
|
+
}>
|
|
16
|
+
|
|
17
|
+
/** Anchored selectable menu; mirrors the subset used from official Menu.d.ts. @author ddj 2026年10月27号 */
|
|
18
|
+
export const Menu: import('react').ComponentType<{
|
|
19
|
+
open: boolean
|
|
20
|
+
anchor: import('react').ReactNode
|
|
21
|
+
items: readonly { id: string; label: import('react').ReactNode; disabled?: boolean }[]
|
|
22
|
+
selectedId?: string
|
|
23
|
+
onSelect: (id: string) => void
|
|
24
|
+
onClose: () => void
|
|
25
|
+
side?: 'top' | 'bottom' | 'right'
|
|
26
|
+
align?: 'start' | 'end'
|
|
27
|
+
portal?: boolean
|
|
28
|
+
compact?: boolean
|
|
29
|
+
autoFocus?: boolean
|
|
30
|
+
}>
|
|
31
|
+
|
|
10
32
|
/** 图标组件通用 props(官方 Icon* 组件与 FileTypeIcon 共用形状)。 */
|
|
11
33
|
export interface EdrvIconProps {
|
|
12
34
|
/** 图标边长(px);省略时用图标自带尺寸。 */
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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 { installCapture } from './officialCapture.js'
|
|
5
|
+
import { installEvidence } from './reviewCapture.js'
|
|
4
6
|
import { closeArchiveDbs } from './archiveDb.js'
|
|
5
7
|
import { handleRpc, integrationBaseUrlOf } from './rpc.js'
|
|
6
8
|
import { newSearcher } from './search/orchestrator.js'
|
|
@@ -67,6 +69,8 @@ function createRuntime(ctx: Ctx, config: unknown) {
|
|
|
67
69
|
* @param runtime Managers created by this instance.
|
|
68
70
|
*/
|
|
69
71
|
function attachRuntime(ctx: Ctx, runtime: Runtime): void {
|
|
72
|
+
const evidence = installEvidence(ctx)
|
|
73
|
+
const stopCapture = installCapture(ctx, (cwd, snapshot) => { evidence.persist(cwd, snapshot); return true })
|
|
70
74
|
/** @author ddj 2026年09月28号 @param exec Tool execution. @param result Tool result. */
|
|
71
75
|
function onResult(exec: unknown, result: unknown): void {
|
|
72
76
|
void captureToolResult(ctx, runtime.registry, exec, result)
|
|
@@ -84,6 +88,8 @@ function attachRuntime(ctx: Ctx, runtime: Runtime): void {
|
|
|
84
88
|
}
|
|
85
89
|
/** @author ddj 2026年09月28号 Release every manager even if another cleanup rejects. */
|
|
86
90
|
async function cleanup(): Promise<void> {
|
|
91
|
+
await stopCapture()
|
|
92
|
+
evidence.dispose()
|
|
87
93
|
runtime.fileVersions.dispose()
|
|
88
94
|
closeArchiveDbs()
|
|
89
95
|
disposeAllServers()
|
package/src/model.ts
CHANGED
|
@@ -277,11 +277,15 @@ export function fileMaxBatch(records: Map<string, DiffRecord>, path: string): nu
|
|
|
277
277
|
return m
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
/**
|
|
280
|
+
/** @public @author ddj 2026年10月26号 @param map Legacy records. @param max Cache target, never a pending retention limit. */
|
|
281
281
|
export function prune(map: Map<string, DiffRecord>, max = MAX_RECORDS): void {
|
|
282
282
|
if (map.size <= max) return
|
|
283
|
-
const sorted = [...map.values()].
|
|
284
|
-
|
|
283
|
+
const sorted = [...map.values()].filter((record) => record.archived && recordResolved(record))
|
|
284
|
+
.sort((a, b) => (a.at < b.at ? -1 : 1))
|
|
285
|
+
for (const record of sorted) {
|
|
286
|
+
if (map.size <= max) break
|
|
287
|
+
map.delete(record.callId)
|
|
288
|
+
}
|
|
285
289
|
}
|
|
286
290
|
|
|
287
291
|
/** 按批次分组记录。 */
|