dsh-vscode-mode 0.25.1 → 0.27.0
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 -11
- package/lib/client.js +1969 -1344
- package/lib/client.js.map +2 -2
- package/lib/index.js +1443 -716
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/compatSummary.ts +4 -0
- package/src/client/contextMenusOfficial.ts +80 -0
- package/src/client/forgeBridge.ts +28 -0
- package/src/client/index.ts +18 -3
- package/src/client/monaco/lsp/lspClient.ts +25 -7
- package/src/client/officialSidebar.ts +15 -3
- package/src/client/reviewBar.ts +3 -0
- package/src/client/reviewBatch.ts +9 -1
- package/src/client/reviewKeepAll.ts +72 -0
- package/src/client/sidebar/menuItems.ts +6 -0
- package/src/client/sidebar/panels/FileExplorer.ts +26 -1
- package/src/client/sidebar/types.ts +3 -0
- package/src/client/state/diffSummary.ts +34 -7
- package/src/client/state/recordsFeed.ts +40 -3
- package/src/client/state/reviewFeed.ts +9 -2
- package/src/client/tabMenu.ts +5 -0
- package/src/client/ui/ClaimRouter.ts +11 -5
- package/src/client/ui/EditorPane.ts +9 -1
- package/src/client/ui/EditorView.ts +164 -39
- package/src/client/ui/OfficialSideTab.ts +13 -5
- package/src/client/ui/ReviewAllConfirm.ts +1 -1
- package/src/index.ts +1 -1
- package/src/pending/capture.ts +96 -47
- package/src/pending/persist.ts +94 -5
- package/src/pending/store.ts +171 -58
- package/src/pendingCompat.ts +72 -9
- package/src/pendingRpc.ts +10 -10
- package/src/reviewCapture.ts +64 -23
- package/src/reviewDb.ts +119 -8
- package/src/reviewEvidence.ts +1 -1
- package/src/reviewRpc.ts +115 -9
- package/src/rpc.ts +14 -10
- package/src/shared/review.ts +21 -0
- package/src/shared/rpc.ts +3 -1
- package/src/store.ts +44 -0
- package/src/toolTargets.ts +63 -0
|
@@ -4,7 +4,7 @@ import type { ReviewPage } from '../../shared/review.js'
|
|
|
4
4
|
|
|
5
5
|
export interface ReviewFeed { page: ReviewPage | null; error: string }
|
|
6
6
|
type Listener = (feed: ReviewFeed) => void
|
|
7
|
-
interface Entry { listeners: Set<Listener>; value: ReviewFeed; controller: AbortController; timer?: ReturnType<typeof setTimeout>; loading: boolean; again: boolean; onRefresh: () => void }
|
|
7
|
+
interface Entry { listeners: Set<Listener>; value: ReviewFeed; controller: AbortController; timer?: ReturnType<typeof setTimeout>; loading: boolean; again: boolean; onRefresh: () => void; onVisible?: () => void }
|
|
8
8
|
const feeds = new Map<string, Entry>()
|
|
9
9
|
|
|
10
10
|
/** @private @author ddj 2026年10月26号 @param entry Shared feed. Notify mounted observers only. */
|
|
@@ -13,6 +13,7 @@ function notify(entry: Entry): void { for (const listener of entry.listeners) li
|
|
|
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
14
|
async function refresh(sessionId: string, entry: Entry, force = false): Promise<void> {
|
|
15
15
|
if (entry.controller.signal.aborted) return
|
|
16
|
+
if (typeof document !== 'undefined' && document.hidden) { entry.again = true; return }
|
|
16
17
|
if (entry.loading) { entry.again = true; return }
|
|
17
18
|
clearTimeout(entry.timer)
|
|
18
19
|
entry.loading = true
|
|
@@ -46,14 +47,20 @@ export function subscribeReview(sessionId: string, listener: Listener): () => vo
|
|
|
46
47
|
entry = { listeners: new Set(), value: { page: null, error: '' }, controller: new AbortController(), loading: false, again: false, onRefresh }
|
|
47
48
|
feeds.set(sessionId, entry)
|
|
48
49
|
if (typeof window !== 'undefined') window.addEventListener('edrv:refresh', onRefresh)
|
|
50
|
+
if (typeof document !== 'undefined') {
|
|
51
|
+
const current = entry
|
|
52
|
+
entry.onVisible = () => { if (!document.hidden) void refresh(sessionId, current) }
|
|
53
|
+
document.addEventListener('visibilitychange', entry.onVisible)
|
|
54
|
+
}
|
|
49
55
|
void refresh(sessionId, entry)
|
|
50
56
|
}
|
|
51
57
|
entry.listeners.add(listener); listener(entry.value)
|
|
52
58
|
return () => {
|
|
53
59
|
entry.listeners.delete(listener)
|
|
54
|
-
if (entry.listeners.size) return
|
|
60
|
+
if (entry.listeners.size || feeds.get(sessionId) !== entry) return
|
|
55
61
|
entry.controller.abort(); clearTimeout(entry.timer); feeds.delete(sessionId)
|
|
56
62
|
if (typeof window !== 'undefined') window.removeEventListener('edrv:refresh', entry.onRefresh)
|
|
63
|
+
if (entry.onVisible && typeof document !== 'undefined') document.removeEventListener('visibilitychange', entry.onVisible)
|
|
57
64
|
}
|
|
58
65
|
}
|
|
59
66
|
|
package/src/client/tabMenu.ts
CHANGED
|
@@ -48,6 +48,7 @@ export interface TabMenuState {
|
|
|
48
48
|
canSplit?: boolean
|
|
49
49
|
canFloat?: boolean
|
|
50
50
|
canReturn?: boolean
|
|
51
|
+
forgeReady?: boolean
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
/** 一条页签菜单项(ContextMenu 的展示形状 + 动作 id)。 */
|
|
@@ -122,6 +123,10 @@ export function buildTabMenu(state: TabMenuState): TabMenuEntry[] {
|
|
|
122
123
|
label: '添加到对话',
|
|
123
124
|
disabled: !(hasSession && canAddToConversation),
|
|
124
125
|
},
|
|
126
|
+
...(state.forgeReady ? [
|
|
127
|
+
{ id: 'forge-open', label: '打开 Forge' },
|
|
128
|
+
{ id: 'forge-local', label: '查看本地变更(Forge)' },
|
|
129
|
+
] : []),
|
|
125
130
|
{ id: 'close', label: '关闭', separator: true, ...(closeChord ? { hint: closeChord } : {}) },
|
|
126
131
|
{ id: 'close-others', label: '关闭其他', disabled: nothingToClose(closeOthers(tabs, path, active), tabs) },
|
|
127
132
|
{ id: 'close-right', label: '关闭右侧标签页', disabled: nothingToClose(closeRight(tabs, path, active), tabs) },
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 作者 ddj 2026年09月10号
|
|
9
9
|
*/
|
|
10
10
|
import React from 'react'
|
|
11
|
-
import { forwardToEditor, parseOfficialFileAddress, resolveNavLine } from '../officialSidebar.js'
|
|
11
|
+
import { forwardToEditor, parseOfficialFileAddress, resolveNavLine, resolveNavColumn } from '../officialSidebar.js'
|
|
12
12
|
import { log } from '../log.js'
|
|
13
13
|
|
|
14
14
|
/** 转发尝试次数(含首次)与重试间隔。 */
|
|
@@ -68,17 +68,22 @@ export function createClaimRouter(spec) {
|
|
|
68
68
|
const doneRef = React.useRef(-1)
|
|
69
69
|
|
|
70
70
|
React.useEffect(() => {
|
|
71
|
-
|
|
72
|
-
doneRef.current
|
|
71
|
+
const intent = JSON.stringify([props?.sessionId, tab?.id, revision, navigation?.address])
|
|
72
|
+
if (intent === doneRef.current) return
|
|
73
73
|
const parsed = parseOfficialFileAddress(navigation?.address)
|
|
74
74
|
if (!parsed) {
|
|
75
75
|
setFailed(true)
|
|
76
76
|
return
|
|
77
77
|
}
|
|
78
78
|
const line = resolveNavLine(navigation?.params)
|
|
79
|
+
const column = resolveNavColumn(navigation?.params)
|
|
80
|
+
let alive = true
|
|
81
|
+
setFailed(false)
|
|
79
82
|
let left = FORWARD_ATTEMPTS
|
|
80
83
|
const attempt = () => {
|
|
81
|
-
if (
|
|
84
|
+
if (!alive || tab?.signal?.aborted) return
|
|
85
|
+
if (forwardToEditor(typeof tab?.actions?.openTab === 'function' ? tab.actions : service, parsed.path, line, navigation?.params?.preview, column)) {
|
|
86
|
+
doneRef.current = intent
|
|
82
87
|
closeTab(tab)
|
|
83
88
|
return
|
|
84
89
|
}
|
|
@@ -87,7 +92,8 @@ export function createClaimRouter(spec) {
|
|
|
87
92
|
else setFailed(true)
|
|
88
93
|
}
|
|
89
94
|
attempt()
|
|
90
|
-
|
|
95
|
+
return () => { alive = false }
|
|
96
|
+
}, [revision, props?.sessionId, tab?.id, navigation?.address])
|
|
91
97
|
|
|
92
98
|
if (!failed || typeof fallback !== 'function') return null
|
|
93
99
|
return React.createElement(fallback, props)
|
|
@@ -43,7 +43,15 @@ export function EditorPane(props: PaneProps): React.ReactElement {
|
|
|
43
43
|
/** @private @author ddj 2026年10月14号 @param event Close press. Never start a drag. */
|
|
44
44
|
function stop(event: React.PointerEvent): void { event.stopPropagation() }
|
|
45
45
|
/** @private @author ddj 2026年10月14号 @param event Keyboard activation. */
|
|
46
|
-
function key(event: React.KeyboardEvent): void {
|
|
46
|
+
function key(event: React.KeyboardEvent): void {
|
|
47
|
+
if (event.key === 'ContextMenu' || (event.shiftKey && event.key === 'F10')) {
|
|
48
|
+
event.preventDefault(); event.stopPropagation()
|
|
49
|
+
const rect = event.currentTarget.getBoundingClientRect()
|
|
50
|
+
props.onMenu(tab.path, { clientX: rect.left, clientY: rect.bottom } as React.MouseEvent)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); props.onSelect(group.id, tab.path) }
|
|
54
|
+
}
|
|
47
55
|
return React.createElement('div', {
|
|
48
56
|
key: tab.path, 'data-edrv-tab-path': tab.path, role: 'tab', tabIndex: 0, 'aria-selected': selected,
|
|
49
57
|
className: 'edrv-tab' + (selected ? ' edrv-tab-active' : '') + (tab.pinned ? ' edrv-tab-pinned' : ''),
|
|
@@ -24,10 +24,12 @@ import { dbg, isDebug, rpc } from '../rpc.js'
|
|
|
24
24
|
import { refreshFeed, subscribeErrors, subscribeRecords } from '../state/recordsFeed.js'
|
|
25
25
|
import { subscribeReview, refreshReview } from '../state/reviewFeed.js'
|
|
26
26
|
import { runReviewBatch } from '../reviewBatch.js'
|
|
27
|
+
import { runKeepAll } from '../reviewKeepAll.js'
|
|
28
|
+
import { toastDom } from '../externalOpen.js'
|
|
27
29
|
import { ReviewAllConfirm } from './ReviewAllConfirm.js'
|
|
28
30
|
import { reviewRegions } from '../../shared/reviewRegions.js'
|
|
29
31
|
import { PENDING_PREFIX } from '../../shared/pendingReview.js'
|
|
30
|
-
import {
|
|
32
|
+
import { coveredPathKeys, diffPathKey, diffSummary } from '../state/diffSummary.js'
|
|
31
33
|
import { emitFileChanged, emitRefresh } from '../events.js'
|
|
32
34
|
import { langOf, loadMonaco, snippetLanguageOf } from '../monaco/loader.js'
|
|
33
35
|
import { bindEditActions, runEditAction } from '../monaco/editActions.js'
|
|
@@ -134,7 +136,7 @@ import { clampToolbarPosition, loadToolbarPosition, saveToolbarPosition } from '
|
|
|
134
136
|
import { editorModelPathOf } from '../dap/modelPath.js'
|
|
135
137
|
import { dapTrace } from '../dap/trace.js'
|
|
136
138
|
import { isSessionMissingError, sessionErrorText, sessionMissingDetail, sessionMissingTitle } from '../sessionError.js'
|
|
137
|
-
import { officialDownView, SESSION_DOWN_NOTICE } from '../reviewBar.js'
|
|
139
|
+
import { officialDownView, SESSION_DOWN_NOTICE, SIDECAR_KEEP_DONE } from '../reviewBar.js'
|
|
138
140
|
|
|
139
141
|
/** 官方差异读取失败前缀:仅用于清理同源陈旧错误,不吞并保存类错误。@author ddj 2026年10月27号 */
|
|
140
142
|
const REVIEW_LOAD_ERROR = '官方差异读取失败:'
|
|
@@ -272,11 +274,11 @@ export function EditorView(props) {
|
|
|
272
274
|
const [monacoErr, setMonacoErr] = React.useState(null)
|
|
273
275
|
const [records, setRecords] = React.useState({})
|
|
274
276
|
const [review, setReview] = React.useState({ page: null, error: '' })
|
|
277
|
+
React.useEffect(() => { setReview({ page: null, error: '' }) }, [sessionId])
|
|
275
278
|
React.useEffect(() => {
|
|
276
|
-
|
|
277
|
-
if (!sessionId) return undefined
|
|
279
|
+
if (!sessionId || props.visible === false) return undefined
|
|
278
280
|
return subscribeReview(sessionId, setReview)
|
|
279
|
-
}, [sessionId])
|
|
281
|
+
}, [sessionId, props.visible])
|
|
280
282
|
const [tabs, setTabs] = React.useState([])
|
|
281
283
|
const [active, setDiskActive] = React.useState(null)
|
|
282
284
|
const [planTabs, setPlanTabs] = React.useState([])
|
|
@@ -565,14 +567,17 @@ export function EditorView(props) {
|
|
|
565
567
|
layoutRef.current = layout
|
|
566
568
|
if (!diffRendererRef.current) diffRendererRef.current = createDiffRenderer((sid, t) => dbg(sid, t))
|
|
567
569
|
|
|
570
|
+
// 已有权威通道(官方汇总 / 待审累计基线)在管的路径:旧工具记录不得再画出来,也不得被批量纳入
|
|
571
|
+
const coveredPaths = React.useMemo(() => coveredPathKeys(review.page), [review.page])
|
|
572
|
+
|
|
568
573
|
const currentRecords = React.useMemo(() => {
|
|
569
574
|
const list = []
|
|
570
575
|
for (const rec of Object.values(records)) {
|
|
571
|
-
if (rec.archived === true ||
|
|
576
|
+
if (rec.archived === true || coveredPaths.has(diffPathKey(rec.path))) continue
|
|
572
577
|
if (sameFile(rec.path, active)) list.push(rec)
|
|
573
578
|
}
|
|
574
579
|
return list
|
|
575
|
-
}, [records, active, cwd,
|
|
580
|
+
}, [records, active, cwd, coveredPaths])
|
|
576
581
|
|
|
577
582
|
const reviewOpsRef = React.useRef(new Map())
|
|
578
583
|
// 已自动结案的记录 id:防止「改动已不在文件中」的记录在刷新期间被反复提交判定
|
|
@@ -1449,12 +1454,12 @@ export function EditorView(props) {
|
|
|
1449
1454
|
|
|
1450
1455
|
// 挂载:接入会话级共享订阅(单定时器由 recordsFeed 持有,本组件不再自建 interval)
|
|
1451
1456
|
React.useEffect(() => {
|
|
1452
|
-
if (!sessionId) return undefined
|
|
1457
|
+
if (!sessionId || props.visible === false) return undefined
|
|
1453
1458
|
const offRecords = subscribeRecords(sessionId, applyRecords)
|
|
1454
1459
|
const offErrors = subscribeErrors(sessionId, (message) => setError('list异常:' + message))
|
|
1455
1460
|
refreshRecords()
|
|
1456
1461
|
return () => { offRecords(); offErrors() }
|
|
1457
|
-
}, [sessionId])
|
|
1462
|
+
}, [sessionId, props.visible])
|
|
1458
1463
|
|
|
1459
1464
|
/**
|
|
1460
1465
|
* 展示外部同步提示(按路径+类型去重:同一文件重复回调不重置已关闭的提示)。
|
|
@@ -1555,12 +1560,21 @@ export function EditorView(props) {
|
|
|
1555
1560
|
})
|
|
1556
1561
|
|
|
1557
1562
|
React.useEffect(() => {
|
|
1558
|
-
|
|
1563
|
+
let alive = true
|
|
1564
|
+
let loading = false
|
|
1565
|
+
const publishLsp = (servers) => { if (alive) setLspServers(Array.isArray(servers) ? servers : []) }
|
|
1566
|
+
const refreshLsp = async () => {
|
|
1567
|
+
if (loading || props.visible === false || document.hidden) return
|
|
1568
|
+
loading = true
|
|
1569
|
+
try { publishLsp(await refreshStatus(true)) }
|
|
1570
|
+
finally { loading = false }
|
|
1571
|
+
}
|
|
1559
1572
|
const unsubscribe = onLspProgress(publishLsp)
|
|
1560
|
-
void
|
|
1561
|
-
const timer = setInterval(
|
|
1562
|
-
|
|
1563
|
-
|
|
1573
|
+
void refreshLsp()
|
|
1574
|
+
const timer = setInterval(refreshLsp, 1500)
|
|
1575
|
+
document.addEventListener('visibilitychange', refreshLsp)
|
|
1576
|
+
return () => { alive = false; unsubscribe(); clearInterval(timer); document.removeEventListener('visibilitychange', refreshLsp) }
|
|
1577
|
+
}, [sessionId, props.visible])
|
|
1564
1578
|
|
|
1565
1579
|
// LSP 会话:EditorView 持有权威 sessionId,直接写入 lspClient 并广播。
|
|
1566
1580
|
// 直接调用不依赖事件时序(订阅可能晚于首次派发),广播供其它消费方复用。
|
|
@@ -1604,6 +1618,9 @@ export function EditorView(props) {
|
|
|
1604
1618
|
const fileOpenRef = React.useRef(null)
|
|
1605
1619
|
/** @private @author ddj 2026年10月08号 @param e Routed file request. Always uses the current workspace and save/preview callbacks. */
|
|
1606
1620
|
fileOpenRef.current = (e) => {
|
|
1621
|
+
if (e?.detail?.sessionId && e.detail.sessionId !== sessionId) return
|
|
1622
|
+
if (e?.detail?.ownerId && e.detail.ownerId !== props?.ownerId) return
|
|
1623
|
+
if (!e?.detail?.ownerId && props?.visible === false) return
|
|
1607
1624
|
const p = e?.detail?.path
|
|
1608
1625
|
if (!p) return
|
|
1609
1626
|
// G9:入口统一归一为工作区相对路径(差异栏/对话链接/LSP 跳转等都经此事件)
|
|
@@ -3315,8 +3332,10 @@ export function EditorView(props) {
|
|
|
3315
3332
|
}
|
|
3316
3333
|
// #endregion
|
|
3317
3334
|
if (!res || !res.ok || !Array.isArray(res.results)) {
|
|
3318
|
-
|
|
3319
|
-
|
|
3335
|
+
const failure = res?.error ? String(res.error) : '批量处理失败'
|
|
3336
|
+
setError(failure)
|
|
3337
|
+
// 真实原因要一路带到调用处:调用方按计数兜底会把 Host 原文盖掉,用户只看得到泛化文案
|
|
3338
|
+
return { ok: 0, fail: items.length, stale: 0, error: failure }
|
|
3320
3339
|
}
|
|
3321
3340
|
let ok = 0
|
|
3322
3341
|
let fail = 0
|
|
@@ -3336,8 +3355,9 @@ export function EditorView(props) {
|
|
|
3336
3355
|
if (Object.keys(next).length) setRecords((prev) => Object.assign({}, prev, next))
|
|
3337
3356
|
return { ok, fail, stale }
|
|
3338
3357
|
}).catch((e) => {
|
|
3339
|
-
|
|
3340
|
-
|
|
3358
|
+
const failure = '批量处理异常:' + String(e)
|
|
3359
|
+
setError(failure)
|
|
3360
|
+
return { ok: 0, fail: items.length, stale: 0, error: failure }
|
|
3341
3361
|
})
|
|
3342
3362
|
}
|
|
3343
3363
|
|
|
@@ -3470,6 +3490,33 @@ export function EditorView(props) {
|
|
|
3470
3490
|
finally { batchBusyRef.current = false; if (lifetime?.live) setReviewBatchBusy(false) }
|
|
3471
3491
|
}
|
|
3472
3492
|
|
|
3493
|
+
/**
|
|
3494
|
+
* 批量失败提示:Host 原文优先,只有拿不到原因时才按计数兜底。
|
|
3495
|
+
* @author ddj 2026年11月3号
|
|
3496
|
+
* @param result actMany 的结果
|
|
3497
|
+
* @returns 提示文案;没有失败返回 null
|
|
3498
|
+
*/
|
|
3499
|
+
const batchError = (result) => {
|
|
3500
|
+
if (!result.error) return batchFailText(result)
|
|
3501
|
+
// 会话没在当前端激活:写类 RPC 一律被 requireSession 拒绝(冷读列表却正常),
|
|
3502
|
+
// 只把「会话不存在」原文丢给用户会像新故障,这里改说清「没执行、记录没动、怎么恢复」。
|
|
3503
|
+
if (isSessionMissingError(result.error)) return '这个会话没在当前端激活,Keep/Undo 没有执行(记录未改动);在会话里发一条消息激活后重试'
|
|
3504
|
+
return result.error
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
/**
|
|
3508
|
+
* 批量结果没有 Host 原文时的计数兜底文案。
|
|
3509
|
+
* @author ddj 2026年11月3号
|
|
3510
|
+
* @param result actMany 的结果
|
|
3511
|
+
* @returns 提示文案;没有失败返回 null
|
|
3512
|
+
*/
|
|
3513
|
+
const batchFailText = (result) => {
|
|
3514
|
+
if (!result.fail) return null
|
|
3515
|
+
return result.stale
|
|
3516
|
+
? `${result.fail} 处未处理(其中 ${result.stale} 处已不在文件中,只更新了记录),可刷新后重试`
|
|
3517
|
+
: `${result.fail} 处未处理,可刷新后重试`
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3473
3520
|
const acceptFile = () => {
|
|
3474
3521
|
if (missingNow && !officialDetail) return decideMissing(false)
|
|
3475
3522
|
if (officialDetail) return decideOfficial(false)
|
|
@@ -3479,8 +3526,11 @@ export function EditorView(props) {
|
|
|
3479
3526
|
batchBusyRef.current = false
|
|
3480
3527
|
reloadFile(true)
|
|
3481
3528
|
emitRefresh({ scope, paths: active ? [active] : undefined, kind: 'structure' })
|
|
3482
|
-
|
|
3483
|
-
|
|
3529
|
+
// 能走到这里的都是「历史改动」通道:Keep 只把记录标成已看过,不写盘,
|
|
3530
|
+
// 用「已采纳」会让人以为文件被改过,这里必须换成准确措辞
|
|
3531
|
+
setStatus(result.ok ? `${SIDECAR_KEEP_DONE}(${result.ok} 处)` : decideStatus('已采纳 ', result))
|
|
3532
|
+
const message = batchError(result)
|
|
3533
|
+
if (message) setError(message)
|
|
3484
3534
|
}).catch(() => { batchBusyRef.current = false })
|
|
3485
3535
|
}
|
|
3486
3536
|
const undoFile = () => {
|
|
@@ -3493,7 +3543,8 @@ export function EditorView(props) {
|
|
|
3493
3543
|
reloadFile(true)
|
|
3494
3544
|
emitRefresh({ scope, paths: active ? [active] : undefined, kind: 'structure' })
|
|
3495
3545
|
setStatus(decideStatus('已不采纳 ', result))
|
|
3496
|
-
|
|
3546
|
+
const message = batchError(result)
|
|
3547
|
+
if (message) setError(message)
|
|
3497
3548
|
}).catch(() => { batchBusyRef.current = false })
|
|
3498
3549
|
}
|
|
3499
3550
|
|
|
@@ -3501,7 +3552,7 @@ export function EditorView(props) {
|
|
|
3501
3552
|
const allPending = React.useMemo(() => {
|
|
3502
3553
|
const out = []
|
|
3503
3554
|
for (const rec of Object.values(records)) {
|
|
3504
|
-
if (rec.superseded === true ||
|
|
3555
|
+
if (rec.superseded === true || coveredPaths.has(diffPathKey(rec.path))) continue
|
|
3505
3556
|
const perHunk = Array.isArray(rec.decisions?.perHunk) ? rec.decisions.perHunk : []
|
|
3506
3557
|
const hunks = Array.isArray(rec.hunks) ? rec.hunks : []
|
|
3507
3558
|
for (let i = 0; i < hunks.length; i++) {
|
|
@@ -3555,7 +3606,8 @@ export function EditorView(props) {
|
|
|
3555
3606
|
if (reject || !missingNow) reloadFile(true)
|
|
3556
3607
|
emitRefresh({ scope, paths, kind: 'structure' })
|
|
3557
3608
|
setStatus(decideStatus(reject ? '已不采纳 ' : '已采纳 ', result) + `;删除文件 ${handled.size} 个`)
|
|
3558
|
-
|
|
3609
|
+
const message = batchError(result)
|
|
3610
|
+
if (message) setError(message)
|
|
3559
3611
|
} catch (cause) { if (lifetime?.live) setError(String(cause)) }
|
|
3560
3612
|
finally { batchBusyRef.current = false; if (lifetime?.live) refreshRecords() }
|
|
3561
3613
|
}
|
|
@@ -3592,6 +3644,68 @@ export function EditorView(props) {
|
|
|
3592
3644
|
return false
|
|
3593
3645
|
}
|
|
3594
3646
|
|
|
3647
|
+
/** @private @author ddj 2026年11月03号 @description 批量通道的一次性完整快照(分页口径的 pending 只是页内计数)。 @returns 完整待审页 */
|
|
3648
|
+
async function batchSnapshot() {
|
|
3649
|
+
const res = await rpc('edrv.review.list', { sessionId, all: true })
|
|
3650
|
+
if (!res.ok) throw new Error(res.error)
|
|
3651
|
+
return res.page
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
/** @private @author ddj 2026年11月03号 @param token 起批时的会话寿命 @returns 逐条 Undo 的精确部分完成(冲突处停止) */
|
|
3655
|
+
function undoAllBatch(token) {
|
|
3656
|
+
return runReviewBatch({
|
|
3657
|
+
snapshot: batchSnapshot,
|
|
3658
|
+
detail: async (id) => { const res = await rpc('edrv.review.detail', { sessionId, id }); if (!res.ok) throw new Error(res.error); return res.detail },
|
|
3659
|
+
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' }),
|
|
3660
|
+
guard: () => guardReviewAll(true, token), operation: batchOperation,
|
|
3661
|
+
}, true)
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
/** @private @author ddj 2026年11月03号 @param token 起批时的会话寿命 @returns 一次 RPC 的批量 Keep 结果(跳过项不算失败) */
|
|
3665
|
+
function keepAllBatch(token) {
|
|
3666
|
+
return runKeepAll({
|
|
3667
|
+
snapshot: batchSnapshot,
|
|
3668
|
+
keepAll: (items, batchId) => rpc('edrv.review.keepAll', { sessionId, batchId, items }),
|
|
3669
|
+
guard: () => guardReviewAll(false, token),
|
|
3670
|
+
})
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
/**
|
|
3674
|
+
* @private
|
|
3675
|
+
* @author ddj 2026年11月03号
|
|
3676
|
+
* @description: 批量结果落界面:刷新列表、状态栏计数;Keep 的跳过只做提示不当错误,
|
|
3677
|
+
* Undo 保持「遇冲突停止」口径。
|
|
3678
|
+
* @param result 批量结果
|
|
3679
|
+
* @param reject 本次是 Undo 还是 Keep
|
|
3680
|
+
* @param undoPath 起批时的活动文件
|
|
3681
|
+
* @param undoModel 起批时的编辑器模型
|
|
3682
|
+
* @param undoVersion 起批时的模型版本
|
|
3683
|
+
* @returns 官方段是否完全成功(legacy 通道是否继续)
|
|
3684
|
+
*/
|
|
3685
|
+
function finishReviewAll(result, reject, undoPath, undoModel, undoVersion) {
|
|
3686
|
+
refreshReview(sessionId)
|
|
3687
|
+
if (result.paths.length) emitRefresh({ scope, paths: result.paths, kind: 'structure' })
|
|
3688
|
+
if (reject) {
|
|
3689
|
+
if (result.paths.some((path) => sameFile(path, undoPath)) && activeRef.current === undoPath
|
|
3690
|
+
&& !dirtyRef.current[undoPath] && !savingRef.current.has(undoPath) && undoModel?.getVersionId?.() === undoVersion) reloadFile(true)
|
|
3691
|
+
setStatus(`Undo All:官方记录已完成 ${result.completed} / ${result.total}`)
|
|
3692
|
+
if (result.error) setError(`批量已停止,已完成 ${result.completed} 项,其余未处理。${result.error}`)
|
|
3693
|
+
return !result.error
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
const skipped = result.skipped ?? []
|
|
3697
|
+
setStatus(`Keep All:已保留 ${result.completed} / ${result.total}` + (skipped.length ? `,跳过 ${skipped.length} 条` : ''))
|
|
3698
|
+
if (result.error) setError(`Keep All 未全部完成:${result.error}`)
|
|
3699
|
+
if (skipped.length) toastDom(skippedTip(skipped))
|
|
3700
|
+
return !result.error
|
|
3701
|
+
}
|
|
3702
|
+
|
|
3703
|
+
/** @private @author ddj 2026年11月03号 @param skipped 被跳过的条目 @returns 最多 3 条、余量汇总的提示文案 */
|
|
3704
|
+
function skippedTip(skipped) {
|
|
3705
|
+
const head = skipped.slice(0, 3).map((item) => `${item.display}:${item.reason}`).join(';')
|
|
3706
|
+
return skipped.length > 3 ? `${head};等 ${skipped.length} 条` : head
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3595
3709
|
/** @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. */
|
|
3596
3710
|
async function actReviewAll(reject) {
|
|
3597
3711
|
if (batchBusyRef.current) return
|
|
@@ -3604,23 +3718,9 @@ export function EditorView(props) {
|
|
|
3604
3718
|
batchBusyRef.current = true; setReviewBatchBusy(true)
|
|
3605
3719
|
setStatus(reject ? '正在检查并 Undo 全部待审记录…' : '正在 Keep 全部待审记录…')
|
|
3606
3720
|
try {
|
|
3607
|
-
const result = await
|
|
3608
|
-
snapshot: async () => {
|
|
3609
|
-
const res = await rpc('edrv.review.list', { sessionId, all: true })
|
|
3610
|
-
if (!res.ok) throw new Error(res.error)
|
|
3611
|
-
return res.page
|
|
3612
|
-
},
|
|
3613
|
-
detail: async (id) => { const res = await rpc('edrv.review.detail', { sessionId, id }); if (!res.ok) throw new Error(res.error); return res.detail },
|
|
3614
|
-
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' }),
|
|
3615
|
-
guard: () => guardReviewAll(reject, token), operation: batchOperation,
|
|
3616
|
-
}, reject)
|
|
3721
|
+
const result = reject ? await undoAllBatch(token) : await keepAllBatch(token)
|
|
3617
3722
|
if (!token?.live) return
|
|
3618
|
-
|
|
3619
|
-
if (reject && result.paths.some((path) => sameFile(path, undoPath)) && activeRef.current === undoPath
|
|
3620
|
-
&& !dirtyRef.current[undoPath] && !savingRef.current.has(undoPath) && undoModel?.getVersionId?.() === undoVersion) reloadFile(true)
|
|
3621
|
-
if (result.paths.length) emitRefresh({ scope, paths: result.paths, kind: 'structure' })
|
|
3622
|
-
setStatus(`${reject ? 'Undo All' : 'Keep All'}:官方记录已完成 ${result.completed} / ${result.total}`)
|
|
3623
|
-
if (result.error) { setError(`批量已停止,已完成 ${result.completed} 项,其余未处理。${result.error}`); return }
|
|
3723
|
+
if (!finishReviewAll(result, reject, undoPath, undoModel, undoVersion)) return
|
|
3624
3724
|
guardReviewAll(reject, token)
|
|
3625
3725
|
// Official-owned paths are excluded from allPending; never run legacy writes for official history.
|
|
3626
3726
|
if (allPending.length) { batchBusyRef.current = false; await actAllPending(reject) }
|
|
@@ -4020,6 +4120,8 @@ export function EditorView(props) {
|
|
|
4020
4120
|
}
|
|
4021
4121
|
const tabMenuActions = (path) => ({
|
|
4022
4122
|
'add-to-conversation': () => addRefToChat(path),
|
|
4123
|
+
'forge-open': () => props.forge?.open(sessionId),
|
|
4124
|
+
'forge-local': () => props.forge?.open(sessionId, true),
|
|
4023
4125
|
'split-left': () => splitTab(path, 'left'),
|
|
4024
4126
|
'split-right': () => splitTab(path, 'right'),
|
|
4025
4127
|
'split-top': () => splitTab(path, 'top'),
|
|
@@ -4056,6 +4158,7 @@ export function EditorView(props) {
|
|
|
4056
4158
|
cwd,
|
|
4057
4159
|
hasSession: Boolean(sessionId),
|
|
4058
4160
|
canAddToConversation: Boolean(addToConversation),
|
|
4161
|
+
forgeReady: props.forge?.available() === true,
|
|
4059
4162
|
canSplit: workbench.groups.length < GROUP_LIMIT && !workbench.floats.some((item) => item.group === ownerOf(workbench, tabMenu.path)?.id),
|
|
4060
4163
|
canFloat: workbench.groups.length < GROUP_LIMIT,
|
|
4061
4164
|
canReturn: ownerOf(workbench, tabMenu.path)?.id !== MAIN_GROUP,
|
|
@@ -4354,6 +4457,25 @@ export function EditorView(props) {
|
|
|
4354
4457
|
function consumeDrag() { return dragRef.current?.consumeClick() ?? false }
|
|
4355
4458
|
/** @private @author ddj 2026年10月14号 @param path File. @param event Context trigger. */
|
|
4356
4459
|
function paneMenu(path, event) { setTabMenu({ path, x: event.clientX, y: event.clientY }) }
|
|
4460
|
+
const menuOwner = React.useRef({ sessionId, scope, alive: true })
|
|
4461
|
+
menuOwner.current.sessionId = sessionId
|
|
4462
|
+
menuOwner.current.scope = scope
|
|
4463
|
+
const menuValid = React.useRef(null)
|
|
4464
|
+
React.useEffect(() => {
|
|
4465
|
+
menuOwner.current.alive = props.visible !== false
|
|
4466
|
+
return () => { menuOwner.current.alive = false; if (menuValid.current) props.officialMenus?.invalidate(menuValid.current) }
|
|
4467
|
+
}, [sessionId, scope, props.visible, props.officialMenus])
|
|
4468
|
+
React.useEffect(() => {
|
|
4469
|
+
if (!tabMenu || !props.officialMenus || !sessionId) return
|
|
4470
|
+
const capturedSession = sessionId
|
|
4471
|
+
const capturedScope = scope
|
|
4472
|
+
const path = tabMenu.path
|
|
4473
|
+
const groupId = ownerOf(benchRef.current, path)?.id
|
|
4474
|
+
const valid = () => menuOwner.current.alive && menuOwner.current.sessionId === capturedSession && menuOwner.current.scope === capturedScope && ownerOf(benchRef.current, path)?.id === groupId && tabsRef.current.some((tab) => tab.path === path)
|
|
4475
|
+
menuValid.current = valid
|
|
4476
|
+
const items = tabMenuEntries().map((entry) => ({ ...entry, run: entry.onClick ?? (() => {}) }))
|
|
4477
|
+
if (props.officialMenus.open('tab', sessionId, path, { x: tabMenu.x, y: tabMenu.y }, items, valid)) setTabMenu(null)
|
|
4478
|
+
}, [tabMenu, props.officialMenus, sessionId, scope])
|
|
4357
4479
|
/** @private @author ddj 2026年10月14号 @param group Auxiliary group. Return documents rather than closing them. */
|
|
4358
4480
|
function returnPane(group) { acceptLayout(returnGroup(benchRef.current, group)) }
|
|
4359
4481
|
/** @private @author ddj 2026年10月14号 @param group Float id. @param rect Viewport-safe bounds. */
|
|
@@ -4959,6 +5081,9 @@ export function EditorView(props) {
|
|
|
4959
5081
|
editor: () => editorRef.current,
|
|
4960
5082
|
outlineSources: props.outlineSources,
|
|
4961
5083
|
fileMenuItems: props.fileMenuItems,
|
|
5084
|
+
officialMenus: props.officialMenus,
|
|
5085
|
+
visible: props.visible,
|
|
5086
|
+
forge: props.forge,
|
|
4962
5087
|
addToConversation,
|
|
4963
5088
|
svn: svnStatus,
|
|
4964
5089
|
// SVN 变更:面板列表 + 文件树徽标查表(客户端合成,不改 listDir 契约)
|
|
@@ -11,7 +11,7 @@ import React from 'react'
|
|
|
11
11
|
import { pickSessionScope } from '../sessionScope.js'
|
|
12
12
|
import { EditorView } from './EditorView.js'
|
|
13
13
|
import { setSideEditorMounted } from '../sidebarBridge.js'
|
|
14
|
-
import { editorMountEpoch, markEditorActive, markEditorMounted, parseOfficialFileAddress, resolveNavLine, resolveNavOpen } from '../officialSidebar.js'
|
|
14
|
+
import { editorMountEpoch, markEditorActive, markEditorMounted, parseOfficialFileAddress, resolveNavLine, resolveNavColumn, resolveNavOpen } from '../officialSidebar.js'
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* 官方侧边栏 Tab 正文组件(keyed slot sidebar.right.pane.tab 装配)。
|
|
@@ -22,10 +22,14 @@ export function OfficialSideTab(props) {
|
|
|
22
22
|
const sessionId = props?.sessionId
|
|
23
23
|
const useTabInfo = props?.useTabInfo
|
|
24
24
|
let navigation
|
|
25
|
+
let tab
|
|
26
|
+
let visible = true
|
|
25
27
|
// useTabInfo 是 slot 框架按组件实例生成的 hook,须在组件顶层无条件调用
|
|
26
28
|
if (typeof useTabInfo === 'function') {
|
|
27
29
|
try {
|
|
28
|
-
|
|
30
|
+
tab = useTabInfo()?.tab
|
|
31
|
+
navigation = tab?.navigation
|
|
32
|
+
visible = tab?.visible !== false
|
|
29
33
|
} catch {
|
|
30
34
|
navigation = undefined
|
|
31
35
|
}
|
|
@@ -67,16 +71,16 @@ export function OfficialSideTab(props) {
|
|
|
67
71
|
// file 资源 Tab:地址即文件(解码出真实路径,透传 params.line 行号定位)
|
|
68
72
|
const fileHit = parseOfficialFileAddress(navigation?.address)
|
|
69
73
|
if (fileHit) {
|
|
70
|
-
const line = navigation?.params
|
|
74
|
+
const line = resolveNavLine(navigation?.params)
|
|
71
75
|
window.dispatchEvent(new CustomEvent('edrv:open-editor', {
|
|
72
|
-
detail: { path: fileHit.path, line
|
|
76
|
+
detail: { sessionId, ownerId: tab?.id, path: fileHit.path, line, column: resolveNavColumn(navigation?.params), preview: navigation?.params?.preview },
|
|
73
77
|
}))
|
|
74
78
|
return
|
|
75
79
|
}
|
|
76
80
|
// 页类型 Tab:params 携带打开请求(line = 行引用/工具行跳转的行号定位)
|
|
77
81
|
const request = resolveNavOpen(navigation?.params)
|
|
78
82
|
window.dispatchEvent(new CustomEvent('edrv:open-editor', {
|
|
79
|
-
detail: { path: request.path, focusDiff: request.focusDiff, line: resolveNavLine(navigation?.params), preview: navigation?.params?.preview },
|
|
83
|
+
detail: { sessionId, ownerId: tab?.id, path: request.path, focusDiff: request.focusDiff, line: resolveNavLine(navigation?.params), column: resolveNavColumn(navigation?.params), preview: navigation?.params?.preview },
|
|
80
84
|
}))
|
|
81
85
|
})
|
|
82
86
|
|
|
@@ -84,11 +88,15 @@ export function OfficialSideTab(props) {
|
|
|
84
88
|
key: sessionId,
|
|
85
89
|
sessionId,
|
|
86
90
|
layout: 'side',
|
|
91
|
+
visible,
|
|
92
|
+
ownerId: tab?.id,
|
|
87
93
|
schedule: props?.schedule,
|
|
88
94
|
addToConversation: props?.addToConversation,
|
|
89
95
|
sidebarPanels: props?.sidebarPanels,
|
|
90
96
|
outlineSources: props?.outlineSources,
|
|
91
97
|
fileMenuItems: props?.fileMenuItems,
|
|
98
|
+
officialMenus: props?.officialMenus,
|
|
99
|
+
forge: props?.forge,
|
|
92
100
|
fileOpeners: props?.fileOpeners,
|
|
93
101
|
sessions: props?.sessions,
|
|
94
102
|
})
|
|
@@ -19,7 +19,7 @@ export function ReviewAllConfirm(props: ReviewAllProps): React.ReactElement {
|
|
|
19
19
|
const action = props.reject ? 'Undo All' : 'Keep All'
|
|
20
20
|
const warning = props.reject
|
|
21
21
|
? '撤销会改写磁盘;逐项安全校验,遇到冲突停止,已完成项不会自动恢复。'
|
|
22
|
-
: '
|
|
22
|
+
: '只标记待审修改为保留,不改写文件;个别条目若已变化或需在活跃端判定会被跳过并留在列表里,可逐个处理。'
|
|
23
23
|
const footer = React.createElement(React.Fragment, null,
|
|
24
24
|
React.createElement(Button, { variant: 'ghost', onClick: props.onCancel, autoFocus: true }, '取消'),
|
|
25
25
|
React.createElement(Button, { variant: 'primary', onClick: props.onConfirm }, '确认 ' + action))
|
package/src/index.ts
CHANGED
|
@@ -76,7 +76,7 @@ function attachRuntime(ctx: Ctx, runtime: Runtime): void {
|
|
|
76
76
|
const stopCapture = installCapture(ctx, (cwd, snapshot) => { evidence.persist(cwd, snapshot); return true })
|
|
77
77
|
// 待审累计基线:只有写后内容确实变化才落库,异常一律不打断用户工具调用
|
|
78
78
|
const pending = installPending(ctx, (write) => {
|
|
79
|
-
try { trackPendingCover(write.cwd, write.path, true) } catch { /* 内存登记失败不阻断采集 */ }
|
|
79
|
+
try { trackPendingCover(write.cwd, write.path, true, write.sessionId) } catch { /* 内存登记失败不阻断采集 */ }
|
|
80
80
|
try { recordWrite(write) } catch (error) { log.warn('待审差异落库失败:' + String(error)) }
|
|
81
81
|
})
|
|
82
82
|
/** @author ddj 2026年09月28号 @param exec Tool execution. @param result Tool result. */
|