dsh-vscode-mode 0.23.0 → 0.23.2
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 +7 -3
- package/lib/client.js +146 -67
- package/lib/client.js.map +2 -2
- package/lib/index.js +167 -41
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/reviewBatch.ts +22 -21
- package/src/client/ui/EditorView.ts +110 -16
- package/src/client/ui/OfficialDiffBar.tsx +18 -3
- package/src/client/ui/SidecarDiffBar.tsx +3 -2
- package/src/client/ui/reviewAllRow.tsx +5 -4
- package/src/reviewRpc.ts +149 -36
- package/src/shared/review.ts +2 -1
- package/src/shared/rpc.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.2",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车,界面状态(页签/光标/侧栏/编辑 Tab/导航历史)镜像到 DSH home 跨重启恢复",
|
|
5
5
|
"icon": "assets/icon.svg",
|
|
6
6
|
"keywords": [
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Official bulk decisions use persisted metadata, never the visible history page. @author ddj 2026年10月27号 */
|
|
2
2
|
import type { ReviewDetail, ReviewItem, ReviewPage, ReviewResult } from '../shared/review.js'
|
|
3
|
+
import { PENDING_PREFIX } from '../shared/pendingReview.js'
|
|
3
4
|
|
|
4
5
|
export interface BatchPort {
|
|
5
|
-
|
|
6
|
+
snapshot: () => Promise<ReviewPage>
|
|
6
7
|
detail: (id: string) => Promise<ReviewDetail>
|
|
7
8
|
decide: (detail: ReviewDetail, reject: boolean, operationId: string) => Promise<ReviewResult<{ revision: number }>>
|
|
8
9
|
guard: () => void
|
|
@@ -10,27 +11,23 @@ export interface BatchPort {
|
|
|
10
11
|
}
|
|
11
12
|
export interface BatchResult { completed: number; total: number; paths: string[]; error: string }
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* @public
|
|
16
|
+
* @author ddj 2026年10月31号
|
|
17
|
+
* @description: 取一次完整待审快照。分页计数只用于展示,批量写入必须一次取全,避免把页内数字当成总数。
|
|
18
|
+
* @param port 已鉴权的 RPC 适配器
|
|
19
|
+
* @returns 冻结的完整待审集;任何不一致都在首个写入前中止
|
|
20
|
+
* @throws 快照重复、版本已变或数量不完整
|
|
21
|
+
*/
|
|
22
|
+
export async function loadPending(port: Pick<BatchPort, 'snapshot'>): Promise<ReviewItem[]> {
|
|
23
|
+
const page = await port.snapshot()
|
|
16
24
|
const seen = new Set<string>()
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (page.pending !== expected) throw new Error('待审记录在分页期间变化,请刷新后重试')
|
|
24
|
-
for (const item of page.items) {
|
|
25
|
-
if (seen.has(item.id) || !item.current || item.decision !== 'pending') throw new Error('待审分页发生重复或版本变化,请刷新后重试')
|
|
26
|
-
seen.add(item.id); items.push(item)
|
|
27
|
-
}
|
|
28
|
-
if (page.nextCursor === null) break
|
|
29
|
-
if (cursors.has(page.nextCursor) || page.items.length === 0) throw new Error('待审分页未完整返回,请刷新后重试')
|
|
30
|
-
cursor = page.nextCursor; cursors.add(cursor)
|
|
31
|
-
} while (true)
|
|
32
|
-
if (items.length !== expected) throw new Error('待审记录数量不完整,未执行批量操作')
|
|
33
|
-
return items
|
|
25
|
+
for (const item of page.items) {
|
|
26
|
+
if (seen.has(item.id) || !item.current || item.decision !== 'pending') throw new Error('待审快照发生重复或版本变化,请刷新后重试')
|
|
27
|
+
seen.add(item.id)
|
|
28
|
+
}
|
|
29
|
+
if (page.nextCursor !== null || page.items.length !== page.pending) throw new Error('待审记录数量不完整,未执行批量操作')
|
|
30
|
+
return page.items
|
|
34
31
|
}
|
|
35
32
|
|
|
36
33
|
/** @public @author ddj 2026年10月27号 @param port Guarded existing per-file decision adapter. @param reject Undo instead of Keep. @returns Precise partial result; Undo stops at first conflict, never claims atomic multi-file rollback. */
|
|
@@ -45,6 +42,10 @@ export async function runReviewBatch(port: BatchPort, reject: boolean): Promise<
|
|
|
45
42
|
for (const item of items) {
|
|
46
43
|
port.guard()
|
|
47
44
|
const detail = await port.detail(item.id)
|
|
45
|
+
// 待审条目判定要在会话活跃的一端做:冷端能力位已关闭,这里给出比「版本变化」更准确的原因
|
|
46
|
+
if (detail.capability.live === false && item.id.startsWith(PENDING_PREFIX)) {
|
|
47
|
+
throw new Error(`${item.display}:待审条目需要在会话活跃的一端判定,请先在那一端处理或在本端打开该会话`)
|
|
48
|
+
}
|
|
48
49
|
if (!detail.item.current || detail.item.seq !== item.seq || detail.item.revision !== item.revision
|
|
49
50
|
|| !detail.capability.keep) throw new Error(`${item.display}:审查版本变化,请刷新后重试`)
|
|
50
51
|
if (reject && !detail.capability.undo) throw new Error(`${item.display}:${detail.capability.reason}`)
|
|
@@ -569,6 +569,8 @@ export function EditorView(props) {
|
|
|
569
569
|
}, [records, active, cwd, review.page])
|
|
570
570
|
|
|
571
571
|
const reviewOpsRef = React.useRef(new Map())
|
|
572
|
+
// 已自动结案的记录 id:防止「改动已不在文件中」的记录在刷新期间被反复提交判定
|
|
573
|
+
const settledRef = React.useRef(new Set())
|
|
572
574
|
const [officialDetail, setOfficialDetail] = React.useState(null)
|
|
573
575
|
const [officialRecords, setOfficialRecords] = React.useState([])
|
|
574
576
|
const [selectedReview, setSelectedReview] = React.useState(null)
|
|
@@ -588,6 +590,9 @@ export function EditorView(props) {
|
|
|
588
590
|
.find((path) => sameFile(path, active))
|
|
589
591
|
const coveredPath = review.page?.overview?.coveredPaths.find((path) => sameFile(path, active))
|
|
590
592
|
const reviewPath = coveredPath ?? pendingPath
|
|
593
|
+
dbg(sessionId, `[DEBUG reviewPath] active=${active ?? '(null)'} covered=${coveredPath ?? '(none)'}`
|
|
594
|
+
+ ` pending=${pendingPath ?? '(none)'} coveredCount=${review.page?.overview?.coveredPaths.length ?? -1}`
|
|
595
|
+
+ ` items=${review.page?.items?.length ?? -1} overview=${review.page?.overview ? 1 : 0}`)
|
|
591
596
|
if (!active || !reviewPath) {
|
|
592
597
|
setOfficialDetail(null)
|
|
593
598
|
setOfficialRecords([])
|
|
@@ -601,9 +606,15 @@ export function EditorView(props) {
|
|
|
601
606
|
if (controller.signal.aborted) return
|
|
602
607
|
if (!page.ok) throw new Error(page.error)
|
|
603
608
|
const selected = page.page.items.find((item) => item.id === selectedReview) ?? page.page.items[0]
|
|
609
|
+
dbg(sessionId, `[DEBUG reviewList] path=${reviewPath} items=${page.page.items.length}`
|
|
610
|
+
+ ` waiting=${page.page.pending} selected=${selected?.id ?? '(none)'}`)
|
|
604
611
|
if (!selected) { setOfficialRecords([]); setOfficialDetail(null); return }
|
|
605
612
|
const result = await rpc('edrv.review.detail', { sessionId, id: selected.id }, controller.signal)
|
|
606
613
|
if (controller.signal.aborted) return
|
|
614
|
+
dbg(sessionId, `[DEBUG reviewDetail] id=${selected.id} ok=${result.ok ? 1 : 0}`
|
|
615
|
+
+ ` keep=${result.ok ? (result.detail.capability?.keep === true ? 1 : 0) : '-'}`
|
|
616
|
+
+ ` live=${result.ok ? String(result.detail.capability?.live) : '-'}`
|
|
617
|
+
+ ` err=${result.ok ? '' : String(result.error).slice(0, 120)}`)
|
|
607
618
|
if (!result.ok) throw new Error(result.error)
|
|
608
619
|
setOfficialRecords((previous) => JSON.stringify(previous) === JSON.stringify(page.page.items) ? previous : page.page.items)
|
|
609
620
|
setOfficialDetail((previous) => JSON.stringify(previous) === JSON.stringify(result.detail) ? previous : result.detail)
|
|
@@ -672,6 +683,15 @@ export function EditorView(props) {
|
|
|
672
683
|
const noBlocks = !!officialDetail && (officialDetail.diff.kind !== 'text' || officialDetail.diff.hunks.length === 0)
|
|
673
684
|
// 单文件 Keep/Undo 的作用域:可定位差异 + 无法定位的冲突差异(后者否则永久留在待处理列表)
|
|
674
685
|
const decideRegions = React.useMemo(() => [...pendingRegions, ...staleRegions], [pendingRegions, staleRegions])
|
|
686
|
+
// 记录的改动已全部不在当前文件中时没有可见差异可审:自动结案,不再让「改动已失效」占用差异栏。
|
|
687
|
+
React.useEffect(() => {
|
|
688
|
+
if (!officialDetail || contentReady !== true || batchBusyRef.current) return
|
|
689
|
+
if (pendingRegions.length > 0 || staleRegions.length === 0) return
|
|
690
|
+
if (noBlocks || officialDetail.capability?.keep !== true) return
|
|
691
|
+
if (dirtyRef.current[active]) return
|
|
692
|
+
|
|
693
|
+
void settleInvisibleRecord(officialDetail)
|
|
694
|
+
}, [officialDetail, contentReady, pendingRegions, staleRegions, noBlocks, active])
|
|
675
695
|
hoverRegionsRef.current = pendingRegions
|
|
676
696
|
// 行 → 差异区域 映射(hover O(1) 命中;每行归属其区域)
|
|
677
697
|
const lineRegionMap = React.useMemo(() => {
|
|
@@ -3303,33 +3323,84 @@ export function EditorView(props) {
|
|
|
3303
3323
|
*/
|
|
3304
3324
|
const itemOf = (r, reject) => ({ callId: r.callId, scope: r.create ? 'call' : 'hunk', hunkIndex: r.idx, decision: reject ? 'rejected' : 'accepted' })
|
|
3305
3325
|
|
|
3326
|
+
/**
|
|
3327
|
+
* @private
|
|
3328
|
+
* @author ddj 2026年10月31号
|
|
3329
|
+
* @description: 提交一条官方记录的判定;operationId 按决策键复用,网络重试保持幂等。
|
|
3330
|
+
* @param detail 目标记录(含冻结的 seq / revision)
|
|
3331
|
+
* @param reject true=Undo
|
|
3332
|
+
* @param blockId 可选的连续块;缺省为整文件
|
|
3333
|
+
* @returns 成功返回提交结果,失败抛出 host 给出的原因
|
|
3334
|
+
*/
|
|
3335
|
+
async function submitOfficialDecision(detail, reject, blockId) {
|
|
3336
|
+
const source = detail.item
|
|
3337
|
+
const opKey = JSON.stringify([sessionId, source.id, source.seq, source.revision, blockId, reject])
|
|
3338
|
+
let operationId = reviewOpsRef.current.get(opKey)
|
|
3339
|
+
if (!operationId) { operationId = crypto.randomUUID(); reviewOpsRef.current.set(opKey, operationId) }
|
|
3340
|
+
|
|
3341
|
+
const result = await rpc('edrv.review.decide', { sessionId, id: source.id, seq: source.seq,
|
|
3342
|
+
revision: source.revision, operationId, scope: blockId ? 'hunk' : 'file', blockId,
|
|
3343
|
+
decision: reject ? 'rejected' : 'accepted' })
|
|
3344
|
+
if (!result.ok) throw new Error(result.error)
|
|
3345
|
+
|
|
3346
|
+
reviewOpsRef.current.delete(opKey)
|
|
3347
|
+
return result
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
/**
|
|
3351
|
+
* @private
|
|
3352
|
+
* @author ddj 2026年10月31号
|
|
3353
|
+
* @description: 结案一条改动已不在当前文件中的官方记录:等价于 Keep(只标记已采纳,不改写磁盘)。
|
|
3354
|
+
* 这类记录没有可见差异可审,留在差异栏只会让用户去处理一个不存在的东西。
|
|
3355
|
+
* @param detail 待结案的记录
|
|
3356
|
+
*/
|
|
3357
|
+
async function settleInvisibleRecord(detail) {
|
|
3358
|
+
if (batchBusyRef.current) return
|
|
3359
|
+
|
|
3360
|
+
const settled = settledRef.current
|
|
3361
|
+
if (settled.has(detail.item.id)) return
|
|
3362
|
+
settled.add(detail.item.id)
|
|
3363
|
+
|
|
3364
|
+
batchBusyRef.current = true
|
|
3365
|
+
try {
|
|
3366
|
+
await submitOfficialDecision(detail, false)
|
|
3367
|
+
refreshReview(sessionId)
|
|
3368
|
+
} catch (cause) {
|
|
3369
|
+
// 结案失败不打扰用户:记录仍留在待审列表,下次打开该文件再试
|
|
3370
|
+
settled.delete(detail.item.id)
|
|
3371
|
+
dbg(sessionId, `[DEBUG settleInvisible] id=${detail.item.id} err=${String(cause).slice(0, 160)}`)
|
|
3372
|
+
} finally { batchBusyRef.current = false }
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3306
3375
|
/** @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. */
|
|
3307
3376
|
async function decideOfficial(reject, blockId) {
|
|
3308
|
-
|
|
3377
|
+
dbg(sessionId, `[DEBUG decideOfficial] reject=${reject ? 1 : 0} detail=${officialDetail ? 1 : 0}`
|
|
3378
|
+
+ ` busy=${batchBusyRef.current ? 1 : 0} active=${active ?? '(null)'}`)
|
|
3379
|
+
if (!officialDetail || batchBusyRef.current) {
|
|
3380
|
+
// 详情缺失时按钮仍可点(能力位来自列表数据),必须给可读原因而不是静默返回
|
|
3381
|
+
if (!officialDetail && !batchBusyRef.current) setError('官方差异详情尚未加载完成,请稍后重试;若一直如此,请切换文件或刷新页面。')
|
|
3382
|
+
return false
|
|
3383
|
+
}
|
|
3309
3384
|
if (reject && (dirtyRef.current[active] || savingRef.current.has(active) || saveTimerRef.current.isPending())) {
|
|
3310
3385
|
setError('存在未保存或正在保存的修改,请等待完成后重试'); return false
|
|
3311
3386
|
}
|
|
3312
3387
|
batchBusyRef.current = true
|
|
3313
|
-
const
|
|
3388
|
+
const detail = officialDetail
|
|
3314
3389
|
const undoPath = active
|
|
3315
3390
|
const undoModel = editorRef.current?.getModel?.()
|
|
3316
3391
|
const undoVersion = undoModel?.getVersionId?.()
|
|
3317
|
-
const opKey = JSON.stringify([sessionId, source.id, source.seq, source.revision, blockId, reject])
|
|
3318
|
-
let operationId = reviewOpsRef.current.get(opKey)
|
|
3319
|
-
if (!operationId) { operationId = crypto.randomUUID(); reviewOpsRef.current.set(opKey, operationId) }
|
|
3320
3392
|
try {
|
|
3321
|
-
|
|
3322
|
-
revision: source.revision, operationId, scope: blockId ? 'hunk' : 'file', blockId,
|
|
3323
|
-
decision: reject ? 'rejected' : 'accepted' })
|
|
3324
|
-
if (!result.ok) throw new Error(result.error)
|
|
3325
|
-
reviewOpsRef.current.delete(opKey)
|
|
3393
|
+
await submitOfficialDecision(detail, reject, blockId)
|
|
3326
3394
|
if (reject && activeRef.current === undoPath && !dirtyRef.current[undoPath]
|
|
3327
3395
|
&& !savingRef.current.has(undoPath) && undoModel?.getVersionId?.() === undoVersion) reloadFile(true)
|
|
3328
3396
|
else if (reject) setError('Undo 已完成,编辑器在等待期间变化;未覆盖当前缓冲区,请核对磁盘后刷新')
|
|
3329
3397
|
refreshReview(sessionId)
|
|
3330
3398
|
setStatus(reject ? '已 Undo 选中差异' : '已 Keep 选中差异')
|
|
3331
3399
|
return true
|
|
3332
|
-
} catch (cause) {
|
|
3400
|
+
} catch (cause) {
|
|
3401
|
+
dbg(sessionId, `[DEBUG decideOfficialFail] id=${detail.item.id} reject=${reject ? 1 : 0} err=${String(cause).slice(0, 160)}`)
|
|
3402
|
+
setError(String(cause)); return false
|
|
3403
|
+
}
|
|
3333
3404
|
finally { batchBusyRef.current = false }
|
|
3334
3405
|
}
|
|
3335
3406
|
|
|
@@ -3466,10 +3537,26 @@ export function EditorView(props) {
|
|
|
3466
3537
|
return operationId
|
|
3467
3538
|
}
|
|
3468
3539
|
|
|
3540
|
+
/**
|
|
3541
|
+
* @private
|
|
3542
|
+
* @author ddj 2026年10月31号
|
|
3543
|
+
* @description: 批量操作要求官方待审列表已就绪;切换会话后列表还在载入时,
|
|
3544
|
+
* 先触发一次刷新并给出可读提示,而不是直接报错中止(用户看到红色「尚未就绪」会以为功能坏了)。
|
|
3545
|
+
* @param action 面向用户的动作名
|
|
3546
|
+
* @returns 列表已就绪时为 true
|
|
3547
|
+
*/
|
|
3548
|
+
function reviewReadyForBatch(action) {
|
|
3549
|
+
if (review.page?.overview) return true
|
|
3550
|
+
|
|
3551
|
+
refreshReview(sessionId)
|
|
3552
|
+
setStatus(`官方待审列表正在载入,载入完成后请再次点击${action}`)
|
|
3553
|
+
return false
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3469
3556
|
/** @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. */
|
|
3470
3557
|
async function actReviewAll(reject) {
|
|
3471
3558
|
if (batchBusyRef.current) return
|
|
3472
|
-
if (!
|
|
3559
|
+
if (!reviewReadyForBatch(reject ? 'Undo All' : 'Keep All')) return
|
|
3473
3560
|
if (!sum.pendingFiles.length) return
|
|
3474
3561
|
const token = reviewLifeRef.current
|
|
3475
3562
|
const undoPath = activeRef.current
|
|
@@ -3479,7 +3566,11 @@ export function EditorView(props) {
|
|
|
3479
3566
|
setStatus(reject ? '正在检查并 Undo 全部待审记录…' : '正在 Keep 全部待审记录…')
|
|
3480
3567
|
try {
|
|
3481
3568
|
const result = await runReviewBatch({
|
|
3482
|
-
|
|
3569
|
+
snapshot: async () => {
|
|
3570
|
+
const res = await rpc('edrv.review.list', { sessionId, all: true })
|
|
3571
|
+
if (!res.ok) throw new Error(res.error)
|
|
3572
|
+
return res.page
|
|
3573
|
+
},
|
|
3483
3574
|
detail: async (id) => { const res = await rpc('edrv.review.detail', { sessionId, id }); if (!res.ok) throw new Error(res.error); return res.detail },
|
|
3484
3575
|
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' }),
|
|
3485
3576
|
guard: () => guardReviewAll(reject, token), operation: batchOperation,
|
|
@@ -3500,7 +3591,7 @@ export function EditorView(props) {
|
|
|
3500
3591
|
/** @private @author ddj 2026年10月27号 @param reject Whether to undo pending files. @description Open a DSH confirmation without starting any mutations. */
|
|
3501
3592
|
function askReviewAll(reject) {
|
|
3502
3593
|
if (batchBusyRef.current || !sum.pendingFiles.length) return
|
|
3503
|
-
if (!
|
|
3594
|
+
if (!reviewReadyForBatch(reject ? 'Undo All' : 'Keep All')) return
|
|
3504
3595
|
setReviewAllPrompt({ reject, count: sum.pendingFiles.length, token: reviewLifeRef.current })
|
|
3505
3596
|
}
|
|
3506
3597
|
|
|
@@ -4693,6 +4784,9 @@ export function EditorView(props) {
|
|
|
4693
4784
|
const sessionDown = isSessionMissingError(rawReviewError)
|
|
4694
4785
|
const reviewErrorTone = sessionDown ? 'info' : 'error'
|
|
4695
4786
|
const reviewErrorText = !rawReviewError ? undefined : sessionDown ? SESSION_DOWN_NOTICE : sessionErrorText(rawReviewError)
|
|
4787
|
+
// 官方元数据未到位且没有报错 = 首次载入中:此时还不知道哪些历史记录会被官方通道接管,
|
|
4788
|
+
// 直接按历史记录报数会在每次重启时闪出几十个「待处理文件」,故先显示占位、批量也先不放开。
|
|
4789
|
+
const reviewLoading = review.page === null && !review.error
|
|
4696
4790
|
const buildDockSnapshot = () => ({
|
|
4697
4791
|
mode: editorDockMode(active),
|
|
4698
4792
|
missingDeletion: !!missingNow,
|
|
@@ -4714,7 +4808,7 @@ export function EditorView(props) {
|
|
|
4714
4808
|
onUndoFile: undoFile,
|
|
4715
4809
|
onAcceptAllFiles: acceptAllFiles,
|
|
4716
4810
|
onUndoAllFiles: undoAllFiles,
|
|
4717
|
-
allPendingCount: allPending.length + (review.page?.pending ?? 0),
|
|
4811
|
+
allPendingCount: reviewLoading ? 0 : allPending.length + (review.page?.pending ?? 0),
|
|
4718
4812
|
reviewBatchBusy,
|
|
4719
4813
|
batchUnavailable: !review.page?.overview ? '官方待审列表尚未就绪' : undefined,
|
|
4720
4814
|
onRollback: rollbackFile,
|
|
@@ -4728,7 +4822,7 @@ export function EditorView(props) {
|
|
|
4728
4822
|
contentReady,
|
|
4729
4823
|
diffTotal: displayDiffTotal(contentReady, pendingRegions.length, sum.files.find((file) => sameFile(file.path, active))?.pending ?? 0),
|
|
4730
4824
|
fileIdx,
|
|
4731
|
-
fileTotal: sum.pendingFiles.length,
|
|
4825
|
+
fileTotal: reviewLoading ? null : sum.pendingFiles.length,
|
|
4732
4826
|
onPrevDiff: () => gotoDiff(-1),
|
|
4733
4827
|
onNextDiff: () => gotoDiff(1),
|
|
4734
4828
|
onPrevFile: () => gotoFile(-1),
|
|
@@ -15,7 +15,8 @@ export interface OfficialBarProps {
|
|
|
15
15
|
diffIdx?: number
|
|
16
16
|
/** 编辑器内容是否就绪;未就绪时不得断言差异失效 */
|
|
17
17
|
contentReady?: boolean
|
|
18
|
-
|
|
18
|
+
/** 待审文件总数;null 表示官方元数据尚未就绪,栏内显示占位 */
|
|
19
|
+
fileTotal?: number | null
|
|
19
20
|
fileIdx?: number
|
|
20
21
|
allPendingCount?: number
|
|
21
22
|
reviewBatchBusy?: boolean
|
|
@@ -51,6 +52,20 @@ export function barIcon(mark: Mark): React.ReactElement {
|
|
|
51
52
|
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>
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* @private
|
|
57
|
+
* @author ddj 2026年10月31号
|
|
58
|
+
* @description: Keep 按钮提示。会话不在本端活跃时 Keep 仍可执行(只写共享审查库),
|
|
59
|
+
* 提示里补一句说明,免得用户把它当成跨端不可用的按钮。
|
|
60
|
+
* @param hint 差异栏给出的 Keep 说明
|
|
61
|
+
* @param live 会话是否在本端活跃
|
|
62
|
+
* @returns 按钮 title 文本
|
|
63
|
+
*/
|
|
64
|
+
function keepTitle(hint: string, live: boolean): string {
|
|
65
|
+
if (live) return hint
|
|
66
|
+
return `${hint}(会话在另一端激活:Keep 写入共享审查库,仍可执行)`
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
/** @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. */
|
|
55
70
|
export function OfficialDiffBar(props: OfficialBarProps): React.ReactElement {
|
|
56
71
|
const { official, pendingRegions = [], staleRegions = [], diffIdx = 0 } = props
|
|
@@ -75,7 +90,7 @@ export function OfficialDiffBar(props: OfficialBarProps): React.ReactElement {
|
|
|
75
90
|
|
|
76
91
|
return <div className="edrv-diffbar edrv-diffbar-dock edrv-reviewbar" data-edrv-diffbox="1" data-edrv-diff-source="official" role="region" aria-label="文件差异审查">
|
|
77
92
|
<ReviewAllRow
|
|
78
|
-
total={props.fileTotal ??
|
|
93
|
+
total={props.fileTotal ?? null}
|
|
79
94
|
current={(props.fileTotal ?? 0) && (props.fileIdx ?? -1) >= 0 ? (props.fileIdx ?? 0) + 1 : 0}
|
|
80
95
|
busy={props.reviewBatchBusy}
|
|
81
96
|
unavailable={props.batchUnavailable}
|
|
@@ -104,7 +119,7 @@ export function OfficialDiffBar(props: OfficialBarProps): React.ReactElement {
|
|
|
104
119
|
</div>
|
|
105
120
|
<div className="edrv-review-actions">
|
|
106
121
|
<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>
|
|
107
|
-
<Button size="sm" variant="primary" className="edrv-review-keep" icon={barIcon('check')} onClick={props.onAcceptFile} disabled={props.reviewBatchBusy || !detail?.capability.keep} title={detail?.capability.keep ? view.keepHint : detail?.capability.reason}>Keep</Button>
|
|
122
|
+
<Button size="sm" variant="primary" className="edrv-review-keep" icon={barIcon('check')} onClick={props.onAcceptFile} disabled={props.reviewBatchBusy || !detail?.capability.keep} title={detail?.capability.keep ? keepTitle(view.keepHint, detail.capability.live !== false) : detail?.capability.reason}>Keep</Button>
|
|
108
123
|
<Menu open={menu === 'more'} anchor={moreTrigger} items={[{ id: 'pending', label: '差异总览' }, { id: 'archive', label: '归档 / 历史' }]} onSelect={openOverview} onClose={closeMenu} side="top" align="end" portal compact autoFocus />
|
|
109
124
|
</div>
|
|
110
125
|
</div>
|
|
@@ -29,7 +29,8 @@ export interface SidecarBarProps {
|
|
|
29
29
|
/** 编辑器内容是否就绪;未就绪时不得断言差异失效 */
|
|
30
30
|
contentReady?: boolean
|
|
31
31
|
fileIdx?: number
|
|
32
|
-
|
|
32
|
+
/** 待审文件总数;null 表示官方元数据尚未就绪,栏内显示占位 */
|
|
33
|
+
fileTotal?: number | null
|
|
33
34
|
allPendingCount?: number
|
|
34
35
|
reviewBatchBusy?: boolean
|
|
35
36
|
/** 官方元数据通道失败时的可操作提示 */
|
|
@@ -86,7 +87,7 @@ export function SidecarDiffBar(props: SidecarBarProps): React.ReactElement {
|
|
|
86
87
|
|
|
87
88
|
return <div className="edrv-diffbar edrv-diffbar-dock edrv-reviewbar" data-edrv-diffbox="1" data-edrv-diff-source="sidecar" role="region" aria-label="文件差异审查">
|
|
88
89
|
<ReviewAllRow
|
|
89
|
-
total={props.fileTotal ??
|
|
90
|
+
total={props.fileTotal ?? null}
|
|
90
91
|
current={(props.fileTotal ?? 0) && (props.fileIdx ?? -1) >= 0 ? (props.fileIdx ?? 0) + 1 : 0}
|
|
91
92
|
busy={busy}
|
|
92
93
|
pending={props.allPendingCount}
|
|
@@ -5,8 +5,8 @@ import { Button } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
|
5
5
|
export interface ReviewAllRowProps {
|
|
6
6
|
/** 当前文件序号(1 基,0 表示未定位) */
|
|
7
7
|
current: number
|
|
8
|
-
/**
|
|
9
|
-
total: number
|
|
8
|
+
/** 差异文件总数;null 表示官方元数据尚未就绪、归属还分不清,显示占位而不报个数 */
|
|
9
|
+
total: number | null
|
|
10
10
|
/** 批量操作进行中 */
|
|
11
11
|
busy?: boolean
|
|
12
12
|
/** 批量不可用原因(悬浮提示) */
|
|
@@ -34,12 +34,13 @@ export interface ReviewAllRowProps {
|
|
|
34
34
|
* @returns 持久化的全工作区控制条
|
|
35
35
|
*/
|
|
36
36
|
export function ReviewAllRow(props: ReviewAllRowProps): React.ReactElement {
|
|
37
|
-
const
|
|
37
|
+
const known = typeof props.total === 'number'
|
|
38
|
+
const total = known ? props.total as number : 0
|
|
38
39
|
const current = total && props.current > 0 ? props.current : 0
|
|
39
40
|
const disabled = props.busy || !!props.unavailable || !props.pending
|
|
40
41
|
|
|
41
42
|
return <div className="edrv-review-global" role="group" aria-label="全部待审文件">
|
|
42
|
-
<Button size="sm" variant="ghost" className="edrv-review-total" onClick={props.onOpen} title="打开当前工作区全部待审差异文件">差异文件 <strong>{current} / {total}</strong></Button>
|
|
43
|
+
<Button size="sm" variant="ghost" className="edrv-review-total" onClick={props.onOpen} title="打开当前工作区全部待审差异文件">差异文件 <strong>{known ? `${current} / ${total}` : '—'}</strong></Button>
|
|
43
44
|
<span className="edrv-review-all-actions">
|
|
44
45
|
<Button size="sm" variant="ghost" onClick={props.onKeepAll} disabled={disabled || !props.onKeepAll} title={props.unavailable || props.keepTitle || 'Keep 当前工作区全部待审文件'}>Keep All</Button>
|
|
45
46
|
<Button size="sm" variant="ghost" className="edrv-review-undo" onClick={props.onUndoAll} disabled={disabled || !props.onUndoAll} title={props.unavailable || props.undoTitle || 'Undo 当前工作区全部待审文件'}>Undo All</Button>
|
package/src/reviewRpc.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Session-scoped official review RPC. @author ddj 2026年10月26号 */
|
|
2
2
|
import type { RpcHandlerMap } from './shared/rpc.js'
|
|
3
|
+
import type { ReviewItem, ReviewPage } from './shared/review.js'
|
|
3
4
|
import type { PendingPage } from './shared/pendingReview.js'
|
|
4
5
|
import { decideReview, reviewDetail, reviewPage } from './reviewDb.js'
|
|
5
6
|
import { undoReview } from './reviewUndo.js'
|
|
@@ -8,6 +9,50 @@ import { PENDING_PREFIX, mergedPage, pendingDetailOf, pendingItemOf, pendingSame
|
|
|
8
9
|
import { bucketOf, resolveScope, sessionOf } from './registry.js'
|
|
9
10
|
import type { Registry } from './registry.js'
|
|
10
11
|
import type { Ctx } from './store.js'
|
|
12
|
+
import { log } from './log.js'
|
|
13
|
+
|
|
14
|
+
/** 批量写入快照的硬上限:待审积压超过它说明状态异常,宁可拒绝也不做无界写循环。 */
|
|
15
|
+
const BATCH_LIMIT = 2000
|
|
16
|
+
|
|
17
|
+
/** 冷端 Undo 说明:Undo 会校验证据并写回源文件,只能在持有该会话的那一端执行。 */
|
|
18
|
+
const UNDO_NEEDS_LIVE = 'Undo 需要在本端活跃的会话上执行:该会话当前在另一端(Web / 桌面)激活。'
|
|
19
|
+
+ 'Keep 不受影响,可在任一端执行;或在本端打开该会话后重试。'
|
|
20
|
+
|
|
21
|
+
/** 冷端待审判定说明:待审库是按工作区的本地存储,判定需要与正在编辑的那一端保持一致。 */
|
|
22
|
+
const PENDING_NEEDS_LIVE = '待审条目的判定需要在本端活跃的会话上执行:该会话当前在另一端激活,'
|
|
23
|
+
+ '请在那一端处理,或在本端打开该会话后重试。'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @private
|
|
27
|
+
* @author ddj 2026年10月31号
|
|
28
|
+
* @description: 批量写入用的一次性完整快照。合并口径与列表一致(待审投影优先、同路径官方旧行让位),
|
|
29
|
+
* 但一次取全,且 items 与 pending 同源,客户端才能把它当成写前不变式。
|
|
30
|
+
* @param cwd 工作区
|
|
31
|
+
* @param pending 该会话可见的待审页
|
|
32
|
+
* @returns 完整快照;待审积压超过上限或分页期间数量漂移时返回错误
|
|
33
|
+
*/
|
|
34
|
+
function batchSnapshot(cwd: string, pending: PendingPage): { ok: true; page: ReviewPage } | { ok: false; error: string } {
|
|
35
|
+
const first = reviewPage(cwd)
|
|
36
|
+
if (first.pending > BATCH_LIMIT) return { ok: false, error: `待审记录过多(${first.pending} 条,上限 ${BATCH_LIMIT}),请先分批处理后重试` }
|
|
37
|
+
|
|
38
|
+
const official: ReviewItem[] = []
|
|
39
|
+
let page: ReviewPage | undefined = first
|
|
40
|
+
while (page) {
|
|
41
|
+
for (const item of page.items) if (item.current && item.decision === 'pending') official.push(item)
|
|
42
|
+
page = page.nextCursor === null ? undefined : reviewPage(cwd, page.nextCursor)
|
|
43
|
+
}
|
|
44
|
+
// 分页期间有记录被判定掉:数量对不上就不给客户端一份半截快照
|
|
45
|
+
if (official.length !== first.pending) return { ok: false, error: '待审记录数量不完整,未执行批量操作' }
|
|
46
|
+
|
|
47
|
+
// 待审投影已接管的路径,官方旧行必须让位,否则同一文件会被批量写两次
|
|
48
|
+
const live = pending.items.filter((item) => !item.settled)
|
|
49
|
+
const kept = official.filter((row) => !live.some((entry) => pendingSamePath(entry.path, row.currentPath) || pendingSamePath(entry.path, row.path)))
|
|
50
|
+
const items = [...live.map((entry) => pendingItemOf(entry, cwd)), ...kept]
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
page: { items, pending: items.length, nextCursor: null, gaps: first.gaps, ...(first.overview ? { overview: first.overview } : {}) },
|
|
54
|
+
}
|
|
55
|
+
}
|
|
11
56
|
|
|
12
57
|
/**
|
|
13
58
|
* @public
|
|
@@ -19,20 +64,52 @@ import type { Ctx } from './store.js'
|
|
|
19
64
|
* @returns 审查相关处理器表
|
|
20
65
|
*/
|
|
21
66
|
export function reviewHandlers(ctx: Ctx, registry: Registry): Partial<RpcHandlerMap> {
|
|
22
|
-
/**
|
|
23
|
-
|
|
67
|
+
/**
|
|
68
|
+
* @private
|
|
69
|
+
* @author ddj 2026年10月31号
|
|
70
|
+
* @description: 解析会话作用域,并标明本端是否持有该会话的活跃实例。
|
|
71
|
+
* 工作区一律由 host 解析,绝不采信调用方 cwd;冷读作用域只带 cwd,不带会话实例。
|
|
72
|
+
* @param sessionId 请求作用域
|
|
73
|
+
* @returns 已迁移工作区与活跃标记
|
|
74
|
+
* @throws 会话不存在、会话无工作区或活跃库迁移未完成
|
|
75
|
+
*/
|
|
76
|
+
async function scopeOf(sessionId: string): Promise<{ cwd: string; live: boolean }> {
|
|
24
77
|
const scope = await resolveScope(ctx, sessionId)
|
|
25
78
|
if ('err' in scope) throw new Error('会话不存在或没有工作区')
|
|
79
|
+
|
|
26
80
|
await bucketOf(registry, ctx, scope.cwd)
|
|
27
|
-
return scope.cwd
|
|
81
|
+
return { cwd: scope.cwd, live: !!scope.session }
|
|
28
82
|
}
|
|
29
83
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
84
|
+
/**
|
|
85
|
+
* @private
|
|
86
|
+
* @author ddj 2026年10月31号
|
|
87
|
+
* @description: 本端是否持有该会话的活跃实例(纯诊断用,不解析工作区)。
|
|
88
|
+
* @param sessionId 请求作用域
|
|
89
|
+
* @returns 本端加载了该会话时为 true
|
|
90
|
+
*/
|
|
91
|
+
function liveOf(sessionId: string): boolean {
|
|
92
|
+
return !!sessionOf(ctx, sessionId)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @private
|
|
97
|
+
* @author ddj 2026年10月31号
|
|
98
|
+
* @description: 按本端是否活跃覆写面板能力位。活跃端保持数据库结论;
|
|
99
|
+
* 冷端关闭依赖会话的操作(Undo、待审判定)并给出跨端原因,Keep 仍按数据库结论放行。
|
|
100
|
+
* @param capability 数据库推导的能力位
|
|
101
|
+
* @param live 本端是否持有活跃会话
|
|
102
|
+
* @param pending 是否为待审投影条目
|
|
103
|
+
* @returns 面板可用的能力位
|
|
104
|
+
*/
|
|
105
|
+
function liveCapabilityOf(
|
|
106
|
+
capability: { keep: boolean; undo: boolean; reason: string },
|
|
107
|
+
live: boolean,
|
|
108
|
+
pending: boolean,
|
|
109
|
+
): { keep: boolean; undo: boolean; reason: string; live: boolean } {
|
|
110
|
+
if (live) return { ...capability, live: true }
|
|
111
|
+
if (pending) return { keep: false, undo: false, reason: PENDING_NEEDS_LIVE, live: false }
|
|
112
|
+
return { ...capability, undo: false, live: false, reason: capability.undo ? UNDO_NEEDS_LIVE : capability.reason }
|
|
36
113
|
}
|
|
37
114
|
|
|
38
115
|
/**
|
|
@@ -45,13 +122,23 @@ export function reviewHandlers(ctx: Ctx, registry: Registry): Partial<RpcHandler
|
|
|
45
122
|
* @returns 待审页(失败为空页)
|
|
46
123
|
*/
|
|
47
124
|
function pendingPageOf(cwd: string, sessionId: string): PendingPage {
|
|
48
|
-
try {
|
|
49
|
-
|
|
125
|
+
try {
|
|
126
|
+
const page = loadPage(cwd, sessionId)
|
|
127
|
+
// 已结算(老文本与新文本一致)的投影没有净变化可审:留着会顶掉同路径的官方待审记录,
|
|
128
|
+
// 让差异栏选中一条 Keep 不可用的条目,表现为「按钮亮着却点不动」。
|
|
129
|
+
return page.items.some((item) => item.settled === true)
|
|
130
|
+
? { ...page, items: page.items.filter((item) => item.settled !== true) }
|
|
131
|
+
: page
|
|
132
|
+
} catch { return { items: [], pending: 0, nextCursor: null, gaps: [] } }
|
|
50
133
|
}
|
|
51
134
|
|
|
52
135
|
return {
|
|
53
136
|
'edrv.review.list': async (args) => {
|
|
54
|
-
const cwd = await
|
|
137
|
+
const { cwd } = await scopeOf(args.sessionId)
|
|
138
|
+
// 批量通道要的是一次性完整快照:分页口径的 pending 只是页内展示计数,不能当完整性不变式
|
|
139
|
+
if (args.all && !args.history && args.path === undefined) {
|
|
140
|
+
return batchSnapshot(cwd, pendingPageOf(cwd, args.sessionId))
|
|
141
|
+
}
|
|
55
142
|
const official = reviewPage(cwd, args.cursor, args.history, args.path)
|
|
56
143
|
const pending = pendingPageOf(cwd, args.sessionId)
|
|
57
144
|
// 历史快照只属于官方通道;待审条目没有轮次历史,混进去会造出假的「轮次 0」。
|
|
@@ -66,43 +153,69 @@ export function reviewHandlers(ctx: Ctx, registry: Registry): Partial<RpcHandler
|
|
|
66
153
|
return { ok: true, page: { ...official, items: [...own, ...history] } }
|
|
67
154
|
}
|
|
68
155
|
// 待审投影就位后由它承接被覆盖路径,官方旧行让位,同一路径只保留一个权威
|
|
69
|
-
|
|
156
|
+
const merged = mergedPage(official, pending, cwd)
|
|
157
|
+
log.debug(`[review] list live=${liveOf(args.sessionId) ? 1 : 0} path=${args.path ?? '(all)'}`
|
|
158
|
+
+ ` items=${merged.items.length} pending=${merged.pending} covered=${merged.overview?.coveredPaths.length ?? -1}`)
|
|
159
|
+
return { ok: true, page: merged }
|
|
70
160
|
},
|
|
71
161
|
'edrv.review.detail': async (args) => {
|
|
72
|
-
const
|
|
162
|
+
const scope = await scopeOf(args.sessionId)
|
|
163
|
+
const cwd = scope.cwd
|
|
73
164
|
if (args.id.startsWith(PENDING_PREFIX)) {
|
|
74
165
|
const detail = loadDetail(cwd, args.id.slice(PENDING_PREFIX.length))
|
|
75
166
|
if (!detail) return { ok: false, error: '待审条目不存在' }
|
|
76
|
-
|
|
167
|
+
|
|
168
|
+
const mapped = pendingDetailOf(detail, new Date(detail.item.updatedAt).toISOString(), cwd)
|
|
169
|
+
// 待审判定走本地待审库,跨端会与另一端 agent 的写入抢同一份 JSON,故冷端一律关闭
|
|
170
|
+
return { ok: true, detail: { ...mapped, capability: liveCapabilityOf(mapped.capability, scope.live, true) } }
|
|
77
171
|
}
|
|
172
|
+
|
|
78
173
|
const detail = reviewDetail(cwd, args.id)
|
|
79
|
-
|
|
174
|
+
if (!detail) {
|
|
175
|
+
log.debug(`[review] detail live=${scope.live ? 1 : 0} id=${args.id} found=0`)
|
|
176
|
+
return { ok: false, error: '官方审查记录不存在' }
|
|
177
|
+
}
|
|
178
|
+
// Keep 只写共享库,冷端放行;Undo 依赖会话证据与源文件写回,冷端关闭
|
|
179
|
+
const capability = liveCapabilityOf(detail.capability, scope.live, false)
|
|
180
|
+
log.debug(`[review] detail live=${scope.live ? 1 : 0} id=${args.id} found=1`
|
|
181
|
+
+ ` keep=${capability.keep ? 1 : 0} undo=${capability.undo ? 1 : 0}`)
|
|
182
|
+
return { ok: true, detail: { ...detail, capability } }
|
|
80
183
|
},
|
|
81
184
|
'edrv.review.decide': async (args) => {
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
if (args.id.startsWith(PENDING_PREFIX)) {
|
|
85
|
-
// 待审条目走累计基线:Keep 零写盘,Revert 写回并读回校验
|
|
86
|
-
const result = await applyDecide(ctx.get('fs'), cwd, {
|
|
87
|
-
sessionId: args.sessionId,
|
|
88
|
-
id: args.id.slice(PENDING_PREFIX.length),
|
|
89
|
-
revision: args.revision,
|
|
90
|
-
operationId: args.operationId,
|
|
91
|
-
decision: args.decision,
|
|
92
|
-
scope: args.scope === 'hunk' ? 'block' : 'file',
|
|
93
|
-
...(args.blockId === undefined ? {} : { blockId: args.blockId }),
|
|
94
|
-
})
|
|
95
|
-
if (!result.ok) return { ok: false, error: result.reason }
|
|
96
|
-
return { ok: true, revision: result.flags.revision }
|
|
97
|
-
}
|
|
185
|
+
// Keep 只写共享审查库(CAS + operationId 幂等 + WAL),冷端也放行;
|
|
186
|
+
// Undo 与待审判定依赖本端会话内容或本地待审库,冷端返回可读原因而不是 500。
|
|
98
187
|
try {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
:
|
|
188
|
+
const scope = await scopeOf(args.sessionId)
|
|
189
|
+
if (args.id.startsWith(PENDING_PREFIX)) {
|
|
190
|
+
if (!scope.live) return { ok: false, error: PENDING_NEEDS_LIVE }
|
|
191
|
+
// 待审条目走累计基线:Keep 零写盘,Revert 写回并读回校验
|
|
192
|
+
const result = await applyDecide(ctx.get('fs'), scope.cwd, {
|
|
193
|
+
sessionId: args.sessionId,
|
|
194
|
+
id: args.id.slice(PENDING_PREFIX.length),
|
|
195
|
+
revision: args.revision,
|
|
196
|
+
operationId: args.operationId,
|
|
197
|
+
decision: args.decision,
|
|
198
|
+
scope: args.scope === 'hunk' ? 'block' : 'file',
|
|
199
|
+
...(args.blockId === undefined ? {} : { blockId: args.blockId }),
|
|
200
|
+
})
|
|
201
|
+
if (!result.ok) return { ok: false, error: result.reason }
|
|
202
|
+
return { ok: true, revision: result.flags.revision }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (args.decision === 'rejected') {
|
|
206
|
+
if (!scope.live) return { ok: false, error: UNDO_NEEDS_LIVE }
|
|
207
|
+
const result = await undoReview(ctx, sessionOf(ctx, args.sessionId), scope.cwd, args)
|
|
208
|
+
return { ok: true, ...result }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const result = decideReview(scope.cwd, args)
|
|
212
|
+
if (!scope.live) log.info(`[review] 冷端 Keep 官方记录 ${args.id}`)
|
|
102
213
|
return { ok: true, ...result }
|
|
103
214
|
} catch (error) {
|
|
104
215
|
// Guard failures are review results, not transport failures: preserve their reason.
|
|
105
|
-
|
|
216
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
217
|
+
log.warn(`[review] decide 失败 id=${args.id} decision=${args.decision}: ${message}`)
|
|
218
|
+
return { ok: false, error: message }
|
|
106
219
|
}
|
|
107
220
|
},
|
|
108
221
|
}
|
package/src/shared/review.ts
CHANGED
|
@@ -56,7 +56,8 @@ export interface ReviewDetail {
|
|
|
56
56
|
diff: OfficialDiff
|
|
57
57
|
capturedAt: string
|
|
58
58
|
blocks: import('./reviewRegions.js').ReviewBlockState[]
|
|
59
|
-
|
|
59
|
+
/** live 缺省视为未知:只有 host 明确给出 false 时才代表会话不在本端活跃。 */
|
|
60
|
+
capability: { keep: boolean; undo: boolean; reason: string; live?: boolean }
|
|
60
61
|
}
|
|
61
62
|
export interface CaptureGap {
|
|
62
63
|
sessionId: string
|