dsh-vscode-mode 0.20.0 → 0.20.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 +25 -15
- package/lib/client.js +716 -82
- package/lib/client.js.map +1 -1
- package/lib/index.js +417 -15
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/mcpSessionState.ts +42 -15
- package/src/client/monaco/diffRender.ts +2 -2
- package/src/client/reviewBatch.ts +64 -0
- package/src/client/state/reviewFeed.ts +8 -4
- package/src/client/styles/editor.css +39 -0
- package/src/client/ui/DiffBox.ts +2 -17
- package/src/client/ui/EditorView.ts +176 -12
- package/src/client/ui/McpSessionStatus.ts +22 -7
- package/src/client/ui/OfficialDiffBar.tsx +112 -0
- package/src/client/ui/ReviewAllConfirm.ts +35 -0
- package/src/client-primitives.d.ts +34 -0
- package/src/index.ts +4 -1
- package/src/officialCapture.ts +7 -3
- package/src/reviewCapture.ts +112 -0
- package/src/reviewDb.ts +77 -6
- package/src/reviewEvidence.ts +70 -0
- package/src/reviewRpc.ts +10 -1
- package/src/reviewUndo.ts +104 -0
- package/src/shared/review.ts +5 -1
- package/src/shared/reviewRegions.ts +74 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.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": [
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
import { rpc } from './rpc.js'
|
|
3
3
|
import type { McpSessionStatus } from '../shared/mcp.js'
|
|
4
4
|
|
|
5
|
-
export interface SessionMcpView {
|
|
5
|
+
export interface SessionMcpView {
|
|
6
|
+
status?: McpSessionStatus
|
|
7
|
+
stale: boolean
|
|
8
|
+
/** Query lifecycle only; never a connection failure classification. */
|
|
9
|
+
reason?: 'pending' | 'paused' | 'unavailable'
|
|
10
|
+
/** Browser receipt time of the last accepted snapshot, in epoch milliseconds. */
|
|
11
|
+
observedAt?: number
|
|
12
|
+
}
|
|
6
13
|
type Snapshot = { sessions: McpSessionStatus[]; sampledAt: number }
|
|
7
14
|
type FetchStatus = (signal: AbortSignal) => Promise<Snapshot>
|
|
8
|
-
const unavailable: SessionMcpView = { stale: true }
|
|
9
|
-
const inactive: SessionMcpView = { stale: false }
|
|
10
15
|
|
|
11
16
|
/** One poller for all mounted row/hover consumers; never retains or activates sessions. */
|
|
12
17
|
export class McpSessionState {
|
|
@@ -15,14 +20,14 @@ export class McpSessionState {
|
|
|
15
20
|
private timer?: ReturnType<typeof setTimeout>
|
|
16
21
|
private request?: AbortController
|
|
17
22
|
private epoch = 0
|
|
18
|
-
private
|
|
23
|
+
private fallback: SessionMcpView = { stale: true, reason: 'pending' }
|
|
19
24
|
private disposed = false
|
|
20
25
|
|
|
21
26
|
/** @public @author ddj 2026年10月20号 @param fetchStatus Authenticated read-only query. @param page Visibility boundary, omitted for server tests. */
|
|
22
27
|
constructor(private readonly fetchStatus: FetchStatus, private readonly page?: Document) {}
|
|
23
28
|
|
|
24
29
|
/** @public @author ddj 2026年10月20号 @param sessionId Row identity. @returns Stable view until the next observation. */
|
|
25
|
-
get(sessionId: string): SessionMcpView { return this.views.get(sessionId) ??
|
|
30
|
+
get(sessionId: string): SessionMcpView { return this.views.get(sessionId) ?? this.fallback }
|
|
26
31
|
|
|
27
32
|
/** @public @author ddj 2026年10月20号 @param sessionId Row identity. @param listener React invalidation. @returns Idempotent unsubscribe; the last consumer stops polling. */
|
|
28
33
|
subscribe(sessionId: string, listener: () => void): () => void {
|
|
@@ -33,7 +38,8 @@ export class McpSessionState {
|
|
|
33
38
|
this.listeners.set(sessionId, listeners)
|
|
34
39
|
if (first) {
|
|
35
40
|
this.page?.addEventListener('visibilitychange', this.visibility)
|
|
36
|
-
|
|
41
|
+
if (this.page?.hidden) this.markStale('paused')
|
|
42
|
+
else void this.poll()
|
|
37
43
|
}
|
|
38
44
|
return () => {
|
|
39
45
|
listeners.delete(listener)
|
|
@@ -54,7 +60,7 @@ export class McpSessionState {
|
|
|
54
60
|
clearTimeout(this.timer)
|
|
55
61
|
this.timer = undefined
|
|
56
62
|
this.request?.abort()
|
|
57
|
-
this.
|
|
63
|
+
this.markStale('paused')
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
/** @private @author ddj 2026年10月20号 Cleans listener lifetime as well as timers. */
|
|
@@ -66,32 +72,53 @@ export class McpSessionState {
|
|
|
66
72
|
/** @public @author ddj 2026年10月20号 Permanently disposes HMR-owned polling. */
|
|
67
73
|
dispose(): void { this.disposed = true; this.pause(); this.listeners.clear(); this.views.clear() }
|
|
68
74
|
|
|
69
|
-
/** @private @author ddj 2026年10月
|
|
70
|
-
private publish(snapshot
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
/** @private @author ddj 2026年10月27号 @param snapshot Fresh ledger. Records browser receipt time without changing the host protocol. */
|
|
76
|
+
private publish(snapshot: Snapshot): void {
|
|
77
|
+
const observedAt = Date.now()
|
|
78
|
+
this.fallback = { stale: false, observedAt }
|
|
79
|
+
this.views = new Map(snapshot.sessions.map((status) => [status.sessionId, { status, stale: false, observedAt }]))
|
|
80
|
+
this.notify()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @private @author ddj 2026年10月27号 @param reason Query lifecycle, never a claim about connection health. Preserves stable references for unchanged observations. */
|
|
84
|
+
private markStale(reason: NonNullable<SessionMcpView['reason']>): void {
|
|
85
|
+
if (this.fallback.stale && this.fallback.reason === reason) return
|
|
86
|
+
this.fallback = { ...this.fallback, stale: true, reason }
|
|
87
|
+
for (const [id, view] of this.views) this.views.set(id, { ...view, stale: true, reason })
|
|
88
|
+
this.notify()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @private @author ddj 2026年10月27号 Notifies mounted row and hover consumers after an observation changes. */
|
|
92
|
+
private notify(): void {
|
|
74
93
|
for (const listeners of this.listeners.values()) for (const listener of listeners) listener()
|
|
75
94
|
}
|
|
76
95
|
|
|
77
96
|
/** @private @author ddj 2026年10月20号 Queries once, with a 10-second deadline and a 2.5-second gap after settlement. */
|
|
78
97
|
private async poll(): Promise<void> {
|
|
79
98
|
if (this.disposed || this.request || !this.listeners.size || this.page?.hidden) return
|
|
99
|
+
clearTimeout(this.timer)
|
|
100
|
+
this.timer = undefined
|
|
101
|
+
if (this.fallback.reason === 'paused') this.markStale('pending')
|
|
102
|
+
|
|
80
103
|
const request = new AbortController()
|
|
81
104
|
const epoch = this.epoch
|
|
82
105
|
this.request = request
|
|
83
106
|
/** @author ddj 2026年10月20号 Marks overdue responses unusable even if a transport ignores cancellation. */
|
|
84
|
-
const expire = (): void => { request.abort(); if (epoch === this.epoch) this.
|
|
107
|
+
const expire = (): void => { request.abort(); if (epoch === this.epoch) this.markStale('unavailable') }
|
|
85
108
|
const deadline = setTimeout(expire, 10000)
|
|
86
109
|
try {
|
|
87
110
|
const snapshot = await this.fetchStatus(request.signal)
|
|
88
111
|
if (epoch === this.epoch && !request.signal.aborted) this.publish(snapshot)
|
|
89
112
|
} catch {
|
|
90
|
-
if (epoch === this.epoch) this.
|
|
113
|
+
if (epoch === this.epoch) this.markStale('unavailable')
|
|
91
114
|
} finally {
|
|
92
115
|
clearTimeout(deadline)
|
|
93
116
|
this.request = undefined
|
|
94
|
-
|
|
117
|
+
// A visibility/subscription change resumes promptly, but only after the old transport settles.
|
|
118
|
+
if (!this.disposed && this.listeners.size && !this.page?.hidden) {
|
|
119
|
+
const delay = epoch === this.epoch ? 2500 : 0
|
|
120
|
+
this.timer = setTimeout(() => { void this.poll() }, delay)
|
|
121
|
+
}
|
|
95
122
|
}
|
|
96
123
|
}
|
|
97
124
|
}
|
|
@@ -5,7 +5,7 @@ import { detectColorScheme, officialThemeOf, themeNameOf } from './theme.js'
|
|
|
5
5
|
// #region 显示数据
|
|
6
6
|
/** @private @author ddj 2026年10月09号 @param regions 待显示区域 @returns 内容敏感且无分隔符碰撞的展示键 */
|
|
7
7
|
function renderKey(regions) {
|
|
8
|
-
return JSON.stringify(regions.map((r) => [r.callId, r.idx, r.start, r.end, r.oldStart, r.create, r.oldLines, r.newLines]))
|
|
8
|
+
return JSON.stringify(regions.map((r) => [r.recordId ?? r.callId, r.id ?? r.idx, r.start, r.end, r.oldStart, r.create, r.oldLines, r.newLines]))
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/** @private @author ddj 2026年10月09号 @param node 接收样式的节点 @param colors Monaco 颜色 @returns 设置插件局部变量;不覆盖官方令牌 */
|
|
@@ -48,7 +48,7 @@ function marginRow(line) {
|
|
|
48
48
|
function zoneOf(region) {
|
|
49
49
|
const domNode = document.createElement('div')
|
|
50
50
|
domNode.className = 'edrv-del-zone'
|
|
51
|
-
domNode.dataset.edrvHunk = String(region.callId) + ':' + region.idx
|
|
51
|
+
domNode.dataset.edrvHunk = String(region.recordId ?? region.callId) + ':' + (region.id ?? region.idx)
|
|
52
52
|
const marginDomNode = document.createElement('div')
|
|
53
53
|
marginDomNode.className = 'edrv-del-margin'
|
|
54
54
|
const rows = []
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Official bulk decisions use persisted metadata, never the visible history page. @author ddj 2026年10月27号 */
|
|
2
|
+
import type { ReviewDetail, ReviewItem, ReviewPage, ReviewResult } from '../shared/review.js'
|
|
3
|
+
|
|
4
|
+
export interface BatchPort {
|
|
5
|
+
list: (cursor?: number) => Promise<ReviewPage>
|
|
6
|
+
detail: (id: string) => Promise<ReviewDetail>
|
|
7
|
+
decide: (detail: ReviewDetail, reject: boolean, operationId: string) => Promise<ReviewResult<{ revision: number }>>
|
|
8
|
+
guard: () => void
|
|
9
|
+
operation: (detail: ReviewDetail, reject: boolean) => string
|
|
10
|
+
}
|
|
11
|
+
export interface BatchResult { completed: number; total: number; paths: string[]; error: string }
|
|
12
|
+
|
|
13
|
+
/** @public @author ddj 2026年10月27号 @param port Authenticated RPC adapter. @returns Frozen complete pending set; aborts before any decision if pagination shifts. @throws Incomplete or changing pages. */
|
|
14
|
+
export async function loadPending(port: Pick<BatchPort, 'list'>): Promise<ReviewItem[]> {
|
|
15
|
+
const items: ReviewItem[] = []
|
|
16
|
+
const seen = new Set<string>()
|
|
17
|
+
const cursors = new Set<number>()
|
|
18
|
+
let cursor: number | undefined
|
|
19
|
+
let expected: number | undefined
|
|
20
|
+
do {
|
|
21
|
+
const page = await port.list(cursor)
|
|
22
|
+
expected ??= page.pending
|
|
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
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @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. */
|
|
37
|
+
export async function runReviewBatch(port: BatchPort, reject: boolean): Promise<BatchResult> {
|
|
38
|
+
const result: BatchResult = { completed: 0, total: 0, paths: [], error: '' }
|
|
39
|
+
try {
|
|
40
|
+
port.guard()
|
|
41
|
+
const items = await loadPending(port)
|
|
42
|
+
result.total = items.length
|
|
43
|
+
const details: ReviewDetail[] = []
|
|
44
|
+
// Preflight the entire frozen set before the first mutation. Disk CAS remains authoritative at execution.
|
|
45
|
+
for (const item of items) {
|
|
46
|
+
port.guard()
|
|
47
|
+
const detail = await port.detail(item.id)
|
|
48
|
+
if (!detail.item.current || detail.item.seq !== item.seq || detail.item.revision !== item.revision
|
|
49
|
+
|| !detail.capability.keep) throw new Error(`${item.display}:审查版本变化,请刷新后重试`)
|
|
50
|
+
if (reject && !detail.capability.undo) throw new Error(`${item.display}:${detail.capability.reason}`)
|
|
51
|
+
details.push(detail)
|
|
52
|
+
}
|
|
53
|
+
// Reverse chronological snapshots allow a newer round's result to expose the previous round's endpoint.
|
|
54
|
+
details.sort((left, right) => right.capturedAt.localeCompare(left.capturedAt) || right.item.seq - left.item.seq)
|
|
55
|
+
for (const detail of details) {
|
|
56
|
+
port.guard()
|
|
57
|
+
const reply = await port.decide(detail, reject, port.operation(detail, reject))
|
|
58
|
+
if (!reply.ok) throw new Error(`${detail.item.display}:${reply.error}`)
|
|
59
|
+
result.completed++
|
|
60
|
+
result.paths.push(detail.item.currentPath)
|
|
61
|
+
}
|
|
62
|
+
} catch (cause) { result.error = cause instanceof Error ? cause.message : String(cause) }
|
|
63
|
+
return result
|
|
64
|
+
}
|
|
@@ -11,12 +11,13 @@ const feeds = new Map<string, Entry>()
|
|
|
11
11
|
function notify(entry: Entry): void { for (const listener of entry.listeners) listener(entry.value) }
|
|
12
12
|
|
|
13
13
|
/** @private @author ddj 2026年10月26号 @param sessionId Scope. @param entry Current generation. @returns First-page metadata update; no overlap or late publication. */
|
|
14
|
-
async function refresh(sessionId: string, entry: Entry): Promise<void> {
|
|
14
|
+
async function refresh(sessionId: string, entry: Entry, force = false): Promise<void> {
|
|
15
15
|
if (entry.controller.signal.aborted) return
|
|
16
16
|
if (entry.loading) { entry.again = true; return }
|
|
17
17
|
clearTimeout(entry.timer)
|
|
18
18
|
entry.loading = true
|
|
19
19
|
entry.again = false
|
|
20
|
+
const previous = entry.value
|
|
20
21
|
try {
|
|
21
22
|
const result = await rpc('edrv.review.list', { sessionId }, entry.controller.signal)
|
|
22
23
|
if (!entry.controller.signal.aborted) entry.value = result.ok
|
|
@@ -26,8 +27,11 @@ async function refresh(sessionId: string, entry: Entry): Promise<void> {
|
|
|
26
27
|
finally {
|
|
27
28
|
entry.loading = false
|
|
28
29
|
if (!entry.controller.signal.aborted) {
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
// Polling returns fresh JSON objects even when nothing changed. Preserve identity so
|
|
31
|
+
// editors do not tear down Monaco zones and reload file details every five seconds.
|
|
32
|
+
if (!force && JSON.stringify(previous) === JSON.stringify(entry.value)) entry.value = previous
|
|
33
|
+
else notify(entry)
|
|
34
|
+
if (entry.again) void refresh(sessionId, entry, true)
|
|
31
35
|
else entry.timer = setTimeout(() => { void refresh(sessionId, entry) }, 5000)
|
|
32
36
|
}
|
|
33
37
|
}
|
|
@@ -56,5 +60,5 @@ export function subscribeReview(sessionId: string, listener: Listener): () => vo
|
|
|
56
60
|
/** @public @author ddj 2026年10月26号 @param sessionId Refresh metadata after a decision; does not alter legacy or disk. */
|
|
57
61
|
export function refreshReview(sessionId: string): void {
|
|
58
62
|
const entry = feeds.get(sessionId)
|
|
59
|
-
if (entry) { clearTimeout(entry.timer); void refresh(sessionId, entry) }
|
|
63
|
+
if (entry) { clearTimeout(entry.timer); void refresh(sessionId, entry, true) }
|
|
60
64
|
}
|
|
@@ -271,6 +271,45 @@
|
|
|
271
271
|
[data-edrv-view] .edrv-side-diff-dock .edrv-diffbar-body { max-height: min(180px, 25vh); max-width: 100%; }
|
|
272
272
|
[data-edrv-view] .edrv-side-diff-dock .edrv-diff-details { max-height: min(260px, 35vh); overflow-y: auto; }
|
|
273
273
|
|
|
274
|
+
/* Official review: file context above, navigation/actions below. Theme-owned surfaces; no neon status outlines. @author ddj 2026年10月27号 */
|
|
275
|
+
[data-edrv-diff-dock-shell] .edrv-reviewbar {
|
|
276
|
+
width: 520px; max-width: 100%; min-width: 0; box-sizing: border-box;
|
|
277
|
+
color: var(--dsw-alias-label-primary, #26313d); font-family: var(--dsw-font-family, sans-serif);
|
|
278
|
+
font-size: 12px; line-height: 1.4; container: edrv-review / inline-size;
|
|
279
|
+
}
|
|
280
|
+
[data-edrv-diff-dock-shell] .edrv-review-global { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 4px; padding: 6px 8px; border-bottom: 1px solid var(--dsw-alias-border-l1, #e5e9ef); }
|
|
281
|
+
[data-edrv-diff-dock-shell] .edrv-review-all-actions { display: flex; align-items: center; gap: 4px; }
|
|
282
|
+
[data-edrv-diff-dock-shell] .edrv-review-total strong { font-variant-numeric: tabular-nums; }
|
|
283
|
+
[data-edrv-diff-dock-shell] .edrv-review-heading {
|
|
284
|
+
display: flex; align-items: center; gap: 8px; min-width: 0; padding: 10px 12px 8px;
|
|
285
|
+
border-bottom: 1px solid var(--dsw-alias-border-l1, #e5e9ef);
|
|
286
|
+
}
|
|
287
|
+
[data-edrv-diff-dock-shell] .edrv-review-filemark { display: inline-flex; flex: none; color: var(--dsw-alias-label-tertiary, #697586); }
|
|
288
|
+
[data-edrv-diff-dock-shell] .edrv-review-filename { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 600; }
|
|
289
|
+
[data-edrv-diff-dock-shell] .edrv-review-pending { flex: none; display: inline-flex; gap: 5px; align-items: center; color: var(--dsw-alias-label-secondary, #697586); font-size: 11px; white-space: nowrap; }
|
|
290
|
+
[data-edrv-diff-dock-shell] .edrv-review-pending strong { font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-primary, #26313d); }
|
|
291
|
+
[data-edrv-diff-dock-shell] .edrv-review-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 7px 8px; }
|
|
292
|
+
[data-edrv-diff-dock-shell] .edrv-review-nav { display: flex; align-items: center; flex: none; gap: 1px; }
|
|
293
|
+
[data-edrv-diff-dock-shell] .edrv-review-blocknav { border-left: 1px solid var(--dsw-alias-border-l1, #e5e9ef); padding-left: 6px; }
|
|
294
|
+
[data-edrv-diff-dock-shell] .edrv-review-position { min-width: 32px; text-align: center; font-size: 11px; font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-secondary, #697586); }
|
|
295
|
+
[data-edrv-diff-dock-shell] .edrv-review-actions { display: flex; align-items: center; flex: none; gap: 4px; margin-left: auto; }
|
|
296
|
+
[data-edrv-diff-dock-shell] .edrv-reviewbar button { height: 28px; min-height: 28px; padding: 0 8px; border-radius: 6px; font-family: inherit; font-size: 12px; font-weight: 500; white-space: nowrap; gap: 5px; }
|
|
297
|
+
[data-edrv-diff-dock-shell] .edrv-reviewbar button.edrv-review-icon { width: 28px; min-width: 28px; padding: 0; }
|
|
298
|
+
[data-edrv-diff-dock-shell] .edrv-reviewbar button.edrv-review-record { padding: 0 6px; font-size: 11px; color: var(--dsw-alias-label-secondary, #697586); }
|
|
299
|
+
[data-edrv-diff-dock-shell] .edrv-review-record svg { width: 12px; height: 12px; }
|
|
300
|
+
[data-edrv-diff-dock-shell] .edrv-reviewbar button.edrv-review-keep { background: var(--dsw-alias-brand-primary, #3b63d9); color: var(--dsw-alias-label-primary-inverted, #fff); border: 1px solid transparent; }
|
|
301
|
+
[data-edrv-diff-dock-shell] .edrv-reviewbar button.edrv-review-keep:hover:not(:disabled) { filter: brightness(.94); }
|
|
302
|
+
[data-edrv-diff-dock-shell] .edrv-reviewbar button.edrv-review-undo:hover:not(:disabled) { color: var(--dsw-alias-state-error-primary, #b64043); }
|
|
303
|
+
[data-edrv-diff-dock-shell] .edrv-reviewbar button:disabled { opacity: .4; cursor: not-allowed; }
|
|
304
|
+
[data-edrv-diff-dock-shell] .edrv-reviewbar button:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #3b63d9); outline-offset: 2px; }
|
|
305
|
+
[data-edrv-diff-dock-shell] .edrv-review-notice { padding: 7px 12px; border-top: 1px solid var(--dsw-alias-border-l1, #e5e9ef); color: var(--dsw-alias-label-secondary, #697586); font-size: 11px; overflow-wrap: anywhere; }
|
|
306
|
+
[data-edrv-diff-dock-shell] .edrv-review-error { color: var(--dsw-alias-state-error-primary, #b64043); }
|
|
307
|
+
[data-edrv-diff-dock-shell] .edrv-review-body { border-top: 1px solid var(--dsw-alias-border-l1, #e5e9ef); max-height: min(240px, 30vh); overflow: auto; padding: 8px 12px; }
|
|
308
|
+
@container edrv-review (max-width: 420px) {
|
|
309
|
+
.edrv-reviewbar .edrv-review-heading { gap: 6px; padding-inline: 10px; }
|
|
310
|
+
.edrv-reviewbar .edrv-review-actions { justify-content: flex-end; flex: 1 0 100%; border-top: 1px solid var(--dsw-alias-border-l1, #e5e9ef); padding-top: 6px; }
|
|
311
|
+
}
|
|
312
|
+
|
|
274
313
|
/* ===== 侧边栏(活动栏 + 面板区 + 文件树) ===== */
|
|
275
314
|
[data-edrv-view] .edrv-editor-row { display: flex; flex: 1 1 auto; min-width: 0; min-height: 0; overflow: hidden; }
|
|
276
315
|
[data-edrv-view] .edrv-main-col { box-sizing: border-box; min-width: 0; min-height: 0; overflow: hidden; }
|
package/src/client/ui/DiffBox.ts
CHANGED
|
@@ -8,7 +8,7 @@ import React from 'react'
|
|
|
8
8
|
import { callIdAttr } from '../state/records.js'
|
|
9
9
|
import { canDecideFile, diffDockText } from '../diffDock.js'
|
|
10
10
|
import { badgeOf } from './shared.js'
|
|
11
|
-
import {
|
|
11
|
+
import { OfficialDiffBar } from './OfficialDiffBar.js'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* 折叠控制使用任务栏同尺寸的 chevron 轮廓,避免依赖未声明的运行时图标包。
|
|
@@ -66,22 +66,7 @@ export function DiffBox(props) {
|
|
|
66
66
|
}, diffDockText(fileTotal))))
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
if (props.official)
|
|
70
|
-
const official = props.official
|
|
71
|
-
return React.createElement('div', { className: 'edrv-diffbar edrv-diffbar-dock', 'data-edrv-diffbox': '1', 'data-edrv-diff-source': 'official' },
|
|
72
|
-
React.createElement('div', { className: 'edrv-diffbar-row' },
|
|
73
|
-
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', onClick: onPrevFile, disabled: !fileTotal }, '←'),
|
|
74
|
-
React.createElement('span', null, base + ' · ' + official.pending + ' 条官方待审记录'),
|
|
75
|
-
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', onClick: onNextFile, disabled: !fileTotal }, '→'),
|
|
76
|
-
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', onClick: () => setExpanded(!expanded), 'aria-expanded': expanded }, expanded ? '收起差异' : '查看差异 / 标记已审'),
|
|
77
|
-
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', disabled: true, title: '官方仅提供差异块,缺少可验证全文,不能安全回滚。' }, 'Undo 不可用'),
|
|
78
|
-
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', onClick: () => onOpenLauncher('pending') }, '差异总览'),
|
|
79
|
-
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', onClick: () => onOpenLauncher('archive') }, '归档 / 历史')),
|
|
80
|
-
props.reviewError ? React.createElement('div', { role: 'alert' }, props.reviewError) : null,
|
|
81
|
-
expanded ? React.createElement('div', { className: 'edrv-diffbar-body' },
|
|
82
|
-
React.createElement(ReviewLedger, { key: official.sessionId + ':' + official.path, sessionId: official.sessionId,
|
|
83
|
-
first: official.page, path: official.path })) : null)
|
|
84
|
-
}
|
|
69
|
+
if (props.official) return React.createElement(OfficialDiffBar, props)
|
|
85
70
|
|
|
86
71
|
let bodyEl = null
|
|
87
72
|
if (expanded) {
|
|
@@ -22,7 +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 } from '../state/reviewFeed.js'
|
|
25
|
+
import { subscribeReview, refreshReview } from '../state/reviewFeed.js'
|
|
26
|
+
import { runReviewBatch } from '../reviewBatch.js'
|
|
27
|
+
import { ReviewAllConfirm } from './ReviewAllConfirm.js'
|
|
28
|
+
import { reviewRegions } from '../../shared/reviewRegions.js'
|
|
26
29
|
import { diffSummary, officialPath } from '../state/diffSummary.js'
|
|
27
30
|
import { emitFileChanged, emitRefresh } from '../events.js'
|
|
28
31
|
import { langOf, loadMonaco, snippetLanguageOf } from '../monaco/loader.js'
|
|
@@ -442,6 +445,14 @@ export function EditorView(props) {
|
|
|
442
445
|
const hoverEditorRef = React.useRef(false) // 鼠标是否仍在编辑器命中区
|
|
443
446
|
const hoverPanelRef = React.useRef(false) // 鼠标是否已进入 Keep/Undo 浮层
|
|
444
447
|
const batchBusyRef = React.useRef(false) // 批量 Keep All/Undo All 防重入
|
|
448
|
+
const [reviewBatchBusy, setReviewBatchBusy] = React.useState(false)
|
|
449
|
+
const [reviewAllPrompt, setReviewAllPrompt] = React.useState(null)
|
|
450
|
+
const reviewLifeRef = React.useRef(null)
|
|
451
|
+
React.useEffect(() => {
|
|
452
|
+
const token = { live: true }
|
|
453
|
+
reviewLifeRef.current = token
|
|
454
|
+
return () => { token.live = false }
|
|
455
|
+
}, [sessionId])
|
|
445
456
|
const menuHandlersRef = React.useRef(null) // 右键菜单动作的最新闭包(Monaco addAction 空依赖回调读取)
|
|
446
457
|
const svnMenuDisposersRef = React.useRef([]) // Monaco 右键 SVN 组当前 disposables(同步时先注销旧组)
|
|
447
458
|
// 调试(DAP):store 订阅 tick 驱动重渲 + gutter/F9 切换闭包(ensureEditor 空依赖读取)
|
|
@@ -545,6 +556,44 @@ export function EditorView(props) {
|
|
|
545
556
|
return list
|
|
546
557
|
}, [records, active, cwd, review.page])
|
|
547
558
|
|
|
559
|
+
const reviewOpsRef = React.useRef(new Map())
|
|
560
|
+
const [officialDetail, setOfficialDetail] = React.useState(null)
|
|
561
|
+
const [officialRecords, setOfficialRecords] = React.useState([])
|
|
562
|
+
const [selectedReview, setSelectedReview] = React.useState(null)
|
|
563
|
+
// Only switching file/session/selection invalidates the visible source immediately.
|
|
564
|
+
// Background metadata refresh must not remove and recreate the current Monaco zones.
|
|
565
|
+
React.useEffect(() => {
|
|
566
|
+
setOfficialDetail(null)
|
|
567
|
+
setOfficialRecords([])
|
|
568
|
+
}, [sessionId, active, selectedReview])
|
|
569
|
+
React.useEffect(() => {
|
|
570
|
+
const controller = new AbortController()
|
|
571
|
+
if (!active || !review.page?.overview?.coveredPaths.some((path) => sameFile(path, active))) {
|
|
572
|
+
setOfficialDetail(null)
|
|
573
|
+
setOfficialRecords([])
|
|
574
|
+
return () => controller.abort()
|
|
575
|
+
}
|
|
576
|
+
/** @private @author ddj 2026年10月27号 Load the active file independently of the global history page. */
|
|
577
|
+
async function loadReview() {
|
|
578
|
+
try {
|
|
579
|
+
// Tabs use workspace-relative paths; the metadata index owns the backend identity.
|
|
580
|
+
const reviewPath = review.page?.overview?.coveredPaths.find((path) => sameFile(path, active)) ?? active
|
|
581
|
+
const page = await rpc('edrv.review.list', { sessionId, path: reviewPath }, controller.signal)
|
|
582
|
+
if (controller.signal.aborted) return
|
|
583
|
+
if (!page.ok) throw new Error(page.error)
|
|
584
|
+
const selected = page.page.items.find((item) => item.id === selectedReview) ?? page.page.items[0]
|
|
585
|
+
if (!selected) { setOfficialRecords([]); setOfficialDetail(null); return }
|
|
586
|
+
const result = await rpc('edrv.review.detail', { sessionId, id: selected.id }, controller.signal)
|
|
587
|
+
if (controller.signal.aborted) return
|
|
588
|
+
if (!result.ok) throw new Error(result.error)
|
|
589
|
+
setOfficialRecords((previous) => JSON.stringify(previous) === JSON.stringify(page.page.items) ? previous : page.page.items)
|
|
590
|
+
setOfficialDetail((previous) => JSON.stringify(previous) === JSON.stringify(result.detail) ? previous : result.detail)
|
|
591
|
+
} catch (cause) { if (!controller.signal.aborted) setError('官方差异读取失败:' + String(cause)) }
|
|
592
|
+
}
|
|
593
|
+
void loadReview()
|
|
594
|
+
return () => controller.abort()
|
|
595
|
+
}, [sessionId, active, review.page, selectedReview, sameFile])
|
|
596
|
+
|
|
548
597
|
const contentReady = content !== null && contentPath === active
|
|
549
598
|
// 当前 tab 是否处于图片预览模式(与 loadContent 的分派条件同源;SVG 文本模式时为 false)
|
|
550
599
|
const isImageActive = !!active && isImagePath(active) && !svgTextRef.current.has(active)
|
|
@@ -558,14 +607,16 @@ export function EditorView(props) {
|
|
|
558
607
|
const trace = isDebug() && contentReady && currentRecords.length > 0
|
|
559
608
|
const start = trace ? performance.now() : 0
|
|
560
609
|
// #endregion
|
|
561
|
-
const next =
|
|
610
|
+
const next = officialDetail
|
|
611
|
+
? reviewRegions(officialDetail.diff, officialDetail.item.id, officialDetail.blocks ?? [], contentReady ? content : null)
|
|
612
|
+
: diffRegions(currentRecords, contentReady ? content : null).filter((r) => !r.superseded)
|
|
562
613
|
// #region debug log
|
|
563
614
|
if (trace) {
|
|
564
615
|
try { dbg(sessionId, `[DEBUG diffRegions] records=${currentRecords.length} bytes=${content.length} regions=${next.length} ms=${(performance.now() - start).toFixed(1)}`) } catch (e) { /* 诊断不可影响渲染 */ }
|
|
565
616
|
}
|
|
566
617
|
// #endregion
|
|
567
618
|
return next
|
|
568
|
-
}, [currentRecords, content, contentPath, active])
|
|
619
|
+
}, [currentRecords, content, contentPath, active, officialDetail])
|
|
569
620
|
// useMemo 稳定引用:否则 hover 等重渲染会让 view zone effect 反复重建(- 号闪烁)
|
|
570
621
|
const pendingRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && !r.stale), [regions])
|
|
571
622
|
const staleRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && r.stale), [regions])
|
|
@@ -2529,7 +2580,7 @@ export function EditorView(props) {
|
|
|
2529
2580
|
if (!line || !map.size) { hoverEditorRef.current = false; hideSoon(); return }
|
|
2530
2581
|
const hit = map.get(line)
|
|
2531
2582
|
if (!hit) { hoverEditorRef.current = false; hideSoon(); return }
|
|
2532
|
-
const key = callIdAttr(hit.callId, hit.idx)
|
|
2583
|
+
const key = hit.sourceKind === 'official' ? hit.recordId + ':' + hit.id : callIdAttr(hit.callId, hit.idx)
|
|
2533
2584
|
const top = Math.max(0, ed.getTopForLineNumber(Math.max(1, hit.start)) - ed.getScrollTop())
|
|
2534
2585
|
const layout = ed.getLayoutInfo()
|
|
2535
2586
|
const minimapLeft = layout.minimap?.minimapLeft || 0
|
|
@@ -3013,6 +3064,7 @@ export function EditorView(props) {
|
|
|
3013
3064
|
* @returns Promise<boolean> 是否成功
|
|
3014
3065
|
*/
|
|
3015
3066
|
const actHunk = (region, reject, silent) => {
|
|
3067
|
+
if (region.sourceKind === 'official') return decideOfficial(reject, region.id)
|
|
3016
3068
|
const method = reject ? 'edrv.reject' : 'edrv.accept'
|
|
3017
3069
|
// #region debug log
|
|
3018
3070
|
const trace = isDebug()
|
|
@@ -3113,7 +3165,38 @@ export function EditorView(props) {
|
|
|
3113
3165
|
*/
|
|
3114
3166
|
const itemOf = (r, reject) => ({ callId: r.callId, scope: r.create ? 'call' : 'hunk', hunkIndex: r.idx, decision: reject ? 'rejected' : 'accepted' })
|
|
3115
3167
|
|
|
3168
|
+
/** @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. */
|
|
3169
|
+
async function decideOfficial(reject, blockId) {
|
|
3170
|
+
if (!officialDetail || batchBusyRef.current) return false
|
|
3171
|
+
if (reject && (dirtyRef.current[active] || savingRef.current.has(active) || saveTimerRef.current.isPending())) {
|
|
3172
|
+
setError('存在未保存或正在保存的修改,请等待完成后重试'); return false
|
|
3173
|
+
}
|
|
3174
|
+
batchBusyRef.current = true
|
|
3175
|
+
const source = officialDetail.item
|
|
3176
|
+
const undoPath = active
|
|
3177
|
+
const undoModel = editorRef.current?.getModel?.()
|
|
3178
|
+
const undoVersion = undoModel?.getVersionId?.()
|
|
3179
|
+
const opKey = JSON.stringify([sessionId, source.id, source.seq, source.revision, blockId, reject])
|
|
3180
|
+
let operationId = reviewOpsRef.current.get(opKey)
|
|
3181
|
+
if (!operationId) { operationId = crypto.randomUUID(); reviewOpsRef.current.set(opKey, operationId) }
|
|
3182
|
+
try {
|
|
3183
|
+
const result = await rpc('edrv.review.decide', { sessionId, id: source.id, seq: source.seq,
|
|
3184
|
+
revision: source.revision, operationId, scope: blockId ? 'hunk' : 'file', blockId,
|
|
3185
|
+
decision: reject ? 'rejected' : 'accepted' })
|
|
3186
|
+
if (!result.ok) throw new Error(result.error)
|
|
3187
|
+
reviewOpsRef.current.delete(opKey)
|
|
3188
|
+
if (reject && activeRef.current === undoPath && !dirtyRef.current[undoPath]
|
|
3189
|
+
&& !savingRef.current.has(undoPath) && undoModel?.getVersionId?.() === undoVersion) reloadFile(true)
|
|
3190
|
+
else if (reject) setError('Undo 已完成,编辑器在等待期间变化;未覆盖当前缓冲区,请核对磁盘后刷新')
|
|
3191
|
+
refreshReview(sessionId)
|
|
3192
|
+
setStatus(reject ? '已 Undo 选中差异' : '已 Keep 选中差异')
|
|
3193
|
+
return true
|
|
3194
|
+
} catch (cause) { setError(String(cause)); return false }
|
|
3195
|
+
finally { batchBusyRef.current = false }
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3116
3198
|
const acceptFile = () => {
|
|
3199
|
+
if (officialDetail) return decideOfficial(false)
|
|
3117
3200
|
if (batchBusyRef.current || !decideRegions.length) return
|
|
3118
3201
|
batchBusyRef.current = true
|
|
3119
3202
|
actMany(decideRegions.map((r) => itemOf(r, false))).then((result) => {
|
|
@@ -3125,6 +3208,7 @@ export function EditorView(props) {
|
|
|
3125
3208
|
}).catch(() => { batchBusyRef.current = false })
|
|
3126
3209
|
}
|
|
3127
3210
|
const undoFile = () => {
|
|
3211
|
+
if (officialDetail) return decideOfficial(true)
|
|
3128
3212
|
if (batchBusyRef.current || !decideRegions.length) return
|
|
3129
3213
|
batchBusyRef.current = true
|
|
3130
3214
|
actMany([...decideRegions].reverse().map((r) => itemOf(r, true))).then((result) => {
|
|
@@ -3161,7 +3245,7 @@ export function EditorView(props) {
|
|
|
3161
3245
|
batchBusyRef.current = true
|
|
3162
3246
|
const list = reject ? [...allPending].sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : b.idx - a.idx)) : allPending
|
|
3163
3247
|
const paths = [...new Set(list.map((r) => records[r.callId]?.path).filter(Boolean))]
|
|
3164
|
-
actMany(list.map((r) => itemOf(r, reject))).then((result) => {
|
|
3248
|
+
return actMany(list.map((r) => itemOf(r, reject))).then((result) => {
|
|
3165
3249
|
batchBusyRef.current = false
|
|
3166
3250
|
reloadFile(true)
|
|
3167
3251
|
emitRefresh({ scope, paths: paths.length ? paths : undefined, kind: 'structure' })
|
|
@@ -3169,8 +3253,76 @@ export function EditorView(props) {
|
|
|
3169
3253
|
if (result.fail) setError(result.fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
|
|
3170
3254
|
}).catch(() => { batchBusyRef.current = false })
|
|
3171
3255
|
}
|
|
3172
|
-
|
|
3173
|
-
|
|
3256
|
+
/** @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. */
|
|
3257
|
+
function guardReviewAll(reject, token) {
|
|
3258
|
+
if (!token?.live) throw new Error('编辑器或会话已切换,批量操作已停止')
|
|
3259
|
+
if (reject && (Object.values(dirtyRef.current).some(Boolean) || savingRef.current.size || saveTimerRef.current.isPending())) {
|
|
3260
|
+
throw new Error('存在未保存或正在保存的文件,未执行下一项 Undo,请保存后重试')
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
/** @private @author ddj 2026年10月27号 @param detail Frozen official revision. @param reject Undo action. @returns Stable retry ID shared with single-file decisions. */
|
|
3265
|
+
function batchOperation(detail, reject) {
|
|
3266
|
+
const source = detail.item
|
|
3267
|
+
const key = JSON.stringify([sessionId, source.id, source.seq, source.revision, undefined, reject])
|
|
3268
|
+
let operationId = reviewOpsRef.current.get(key)
|
|
3269
|
+
if (!operationId) { operationId = crypto.randomUUID(); reviewOpsRef.current.set(key, operationId) }
|
|
3270
|
+
return operationId
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
/** @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. */
|
|
3274
|
+
async function actReviewAll(reject) {
|
|
3275
|
+
if (batchBusyRef.current) return
|
|
3276
|
+
if (!review.page?.overview) { setError('官方待审列表尚未就绪,未执行批量操作'); return }
|
|
3277
|
+
if (!sum.pendingFiles.length) 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
|
+
/** @private @author ddj 2026年10月27号 @param reject Whether to undo pending files. @description Open a DSH confirmation without starting any mutations. */
|
|
3305
|
+
function askReviewAll(reject) {
|
|
3306
|
+
if (batchBusyRef.current || !sum.pendingFiles.length) return
|
|
3307
|
+
if (!review.page?.overview) { setError('官方待审列表尚未就绪,未执行批量操作'); return }
|
|
3308
|
+
setReviewAllPrompt({ reject, count: sum.pendingFiles.length, token: reviewLifeRef.current })
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
/** @private @author ddj 2026年10月27号 @description Dismiss the pending confirmation without changing review state. */
|
|
3312
|
+
function cancelReviewAll() {
|
|
3313
|
+
setReviewAllPrompt(null)
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
/** @private @author ddj 2026年10月27号 @description Confirm only within the session lifetime that opened the modal. @returns The guarded batch operation, if still valid. */
|
|
3317
|
+
function confirmReviewAll() {
|
|
3318
|
+
const prompt = reviewAllPrompt
|
|
3319
|
+
setReviewAllPrompt(null)
|
|
3320
|
+
if (!prompt?.token?.live || prompt.token !== reviewLifeRef.current) return
|
|
3321
|
+
return actReviewAll(prompt.reject)
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
const acceptAllFiles = () => askReviewAll(false)
|
|
3325
|
+
const undoAllFiles = () => askReviewAll(true)
|
|
3174
3326
|
const rollbackFile = () => {
|
|
3175
3327
|
if (!active) return
|
|
3176
3328
|
if (!window.confirm('回滚当前文件到修改前状态?(相关差异将归档)')) return
|
|
@@ -4318,7 +4470,10 @@ export function EditorView(props) {
|
|
|
4318
4470
|
},
|
|
4319
4471
|
},
|
|
4320
4472
|
React.createElement('button', { className: 'edrv-pill edrv-pill-keep', onClick: () => { dismissHover(); actHunk(hoverAct.region, false) } }, '✓ Keep'),
|
|
4321
|
-
React.createElement('button', { className: 'edrv-pill edrv-pill-undo',
|
|
4473
|
+
React.createElement('button', { className: 'edrv-pill edrv-pill-undo',
|
|
4474
|
+
disabled: hoverAct.region.sourceKind === 'official' && !officialDetail?.capability.undo,
|
|
4475
|
+
title: hoverAct.region.sourceKind === 'official' ? officialDetail?.capability.reason : undefined,
|
|
4476
|
+
onClick: () => { dismissHover(); actHunk(hoverAct.region, true) } }, '↩ Undo'))
|
|
4322
4477
|
: null
|
|
4323
4478
|
|
|
4324
4479
|
/**
|
|
@@ -4330,8 +4485,9 @@ export function EditorView(props) {
|
|
|
4330
4485
|
const buildDockSnapshot = () => ({
|
|
4331
4486
|
mode: editorDockMode(active),
|
|
4332
4487
|
official: officialFile ? { sessionId, page: review.page, path: officialFile,
|
|
4333
|
-
pending: sum.files.find((file) => sameFile(file.path, active))?.pending ?? 0
|
|
4334
|
-
|
|
4488
|
+
pending: sum.files.find((file) => sameFile(file.path, active))?.pending ?? 0,
|
|
4489
|
+
detail: officialDetail, records: officialRecords, onSelect: setSelectedReview } : null,
|
|
4490
|
+
reviewError: review.error || error,
|
|
4335
4491
|
layout,
|
|
4336
4492
|
pendingRegions,
|
|
4337
4493
|
staleRegions,
|
|
@@ -4340,7 +4496,9 @@ export function EditorView(props) {
|
|
|
4340
4496
|
onUndoFile: undoFile,
|
|
4341
4497
|
onAcceptAllFiles: acceptAllFiles,
|
|
4342
4498
|
onUndoAllFiles: undoAllFiles,
|
|
4343
|
-
allPendingCount: allPending.length,
|
|
4499
|
+
allPendingCount: allPending.length + (review.page?.pending ?? 0),
|
|
4500
|
+
reviewBatchBusy,
|
|
4501
|
+
batchUnavailable: !review.page?.overview ? '官方待审列表尚未就绪' : undefined,
|
|
4344
4502
|
onRollback: rollbackFile,
|
|
4345
4503
|
onJump: jumpTo,
|
|
4346
4504
|
otherFiles,
|
|
@@ -4877,5 +5035,11 @@ export function EditorView(props) {
|
|
|
4877
5035
|
snippetPickerEl,
|
|
4878
5036
|
dapBpMenuEl,
|
|
4879
5037
|
dapBpEditEl)
|
|
4880
|
-
|
|
5038
|
+
const reviewAllEl = reviewAllPrompt?.token?.live && reviewAllPrompt.token === reviewLifeRef.current
|
|
5039
|
+
? React.createElement(ReviewAllConfirm, {
|
|
5040
|
+
reject: reviewAllPrompt.reject, count: reviewAllPrompt.count,
|
|
5041
|
+
onCancel: cancelReviewAll, onConfirm: confirmReviewAll,
|
|
5042
|
+
})
|
|
5043
|
+
: null
|
|
5044
|
+
return React.createElement(React.Fragment, null, rootEl, reviewAllEl)
|
|
4881
5045
|
}
|