dsh-vscode-mode 0.26.0 → 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 +23 -11
- package/lib/client.js +487 -35
- package/lib/client.js.map +2 -2
- package/lib/index.js +398 -183
- 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/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/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 +47 -10
- package/src/client/ui/OfficialSideTab.ts +13 -5
- package/src/pending/capture.ts +96 -47
- package/src/pending/store.ts +95 -57
- package/src/pendingCompat.ts +21 -29
- package/src/pendingRpc.ts +5 -6
- package/src/reviewCapture.ts +64 -23
- package/src/reviewEvidence.ts +1 -1
- package/src/reviewRpc.ts +6 -7
- package/src/toolTargets.ts +63 -0
|
@@ -274,11 +274,11 @@ export function EditorView(props) {
|
|
|
274
274
|
const [monacoErr, setMonacoErr] = React.useState(null)
|
|
275
275
|
const [records, setRecords] = React.useState({})
|
|
276
276
|
const [review, setReview] = React.useState({ page: null, error: '' })
|
|
277
|
+
React.useEffect(() => { setReview({ page: null, error: '' }) }, [sessionId])
|
|
277
278
|
React.useEffect(() => {
|
|
278
|
-
|
|
279
|
-
if (!sessionId) return undefined
|
|
279
|
+
if (!sessionId || props.visible === false) return undefined
|
|
280
280
|
return subscribeReview(sessionId, setReview)
|
|
281
|
-
}, [sessionId])
|
|
281
|
+
}, [sessionId, props.visible])
|
|
282
282
|
const [tabs, setTabs] = React.useState([])
|
|
283
283
|
const [active, setDiskActive] = React.useState(null)
|
|
284
284
|
const [planTabs, setPlanTabs] = React.useState([])
|
|
@@ -1454,12 +1454,12 @@ export function EditorView(props) {
|
|
|
1454
1454
|
|
|
1455
1455
|
// 挂载:接入会话级共享订阅(单定时器由 recordsFeed 持有,本组件不再自建 interval)
|
|
1456
1456
|
React.useEffect(() => {
|
|
1457
|
-
if (!sessionId) return undefined
|
|
1457
|
+
if (!sessionId || props.visible === false) return undefined
|
|
1458
1458
|
const offRecords = subscribeRecords(sessionId, applyRecords)
|
|
1459
1459
|
const offErrors = subscribeErrors(sessionId, (message) => setError('list异常:' + message))
|
|
1460
1460
|
refreshRecords()
|
|
1461
1461
|
return () => { offRecords(); offErrors() }
|
|
1462
|
-
}, [sessionId])
|
|
1462
|
+
}, [sessionId, props.visible])
|
|
1463
1463
|
|
|
1464
1464
|
/**
|
|
1465
1465
|
* 展示外部同步提示(按路径+类型去重:同一文件重复回调不重置已关闭的提示)。
|
|
@@ -1560,12 +1560,21 @@ export function EditorView(props) {
|
|
|
1560
1560
|
})
|
|
1561
1561
|
|
|
1562
1562
|
React.useEffect(() => {
|
|
1563
|
-
|
|
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
|
+
}
|
|
1564
1572
|
const unsubscribe = onLspProgress(publishLsp)
|
|
1565
|
-
void
|
|
1566
|
-
const timer = setInterval(
|
|
1567
|
-
|
|
1568
|
-
|
|
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])
|
|
1569
1578
|
|
|
1570
1579
|
// LSP 会话:EditorView 持有权威 sessionId,直接写入 lspClient 并广播。
|
|
1571
1580
|
// 直接调用不依赖事件时序(订阅可能晚于首次派发),广播供其它消费方复用。
|
|
@@ -1609,6 +1618,9 @@ export function EditorView(props) {
|
|
|
1609
1618
|
const fileOpenRef = React.useRef(null)
|
|
1610
1619
|
/** @private @author ddj 2026年10月08号 @param e Routed file request. Always uses the current workspace and save/preview callbacks. */
|
|
1611
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
|
|
1612
1624
|
const p = e?.detail?.path
|
|
1613
1625
|
if (!p) return
|
|
1614
1626
|
// G9:入口统一归一为工作区相对路径(差异栏/对话链接/LSP 跳转等都经此事件)
|
|
@@ -4108,6 +4120,8 @@ export function EditorView(props) {
|
|
|
4108
4120
|
}
|
|
4109
4121
|
const tabMenuActions = (path) => ({
|
|
4110
4122
|
'add-to-conversation': () => addRefToChat(path),
|
|
4123
|
+
'forge-open': () => props.forge?.open(sessionId),
|
|
4124
|
+
'forge-local': () => props.forge?.open(sessionId, true),
|
|
4111
4125
|
'split-left': () => splitTab(path, 'left'),
|
|
4112
4126
|
'split-right': () => splitTab(path, 'right'),
|
|
4113
4127
|
'split-top': () => splitTab(path, 'top'),
|
|
@@ -4144,6 +4158,7 @@ export function EditorView(props) {
|
|
|
4144
4158
|
cwd,
|
|
4145
4159
|
hasSession: Boolean(sessionId),
|
|
4146
4160
|
canAddToConversation: Boolean(addToConversation),
|
|
4161
|
+
forgeReady: props.forge?.available() === true,
|
|
4147
4162
|
canSplit: workbench.groups.length < GROUP_LIMIT && !workbench.floats.some((item) => item.group === ownerOf(workbench, tabMenu.path)?.id),
|
|
4148
4163
|
canFloat: workbench.groups.length < GROUP_LIMIT,
|
|
4149
4164
|
canReturn: ownerOf(workbench, tabMenu.path)?.id !== MAIN_GROUP,
|
|
@@ -4442,6 +4457,25 @@ export function EditorView(props) {
|
|
|
4442
4457
|
function consumeDrag() { return dragRef.current?.consumeClick() ?? false }
|
|
4443
4458
|
/** @private @author ddj 2026年10月14号 @param path File. @param event Context trigger. */
|
|
4444
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])
|
|
4445
4479
|
/** @private @author ddj 2026年10月14号 @param group Auxiliary group. Return documents rather than closing them. */
|
|
4446
4480
|
function returnPane(group) { acceptLayout(returnGroup(benchRef.current, group)) }
|
|
4447
4481
|
/** @private @author ddj 2026年10月14号 @param group Float id. @param rect Viewport-safe bounds. */
|
|
@@ -5047,6 +5081,9 @@ export function EditorView(props) {
|
|
|
5047
5081
|
editor: () => editorRef.current,
|
|
5048
5082
|
outlineSources: props.outlineSources,
|
|
5049
5083
|
fileMenuItems: props.fileMenuItems,
|
|
5084
|
+
officialMenus: props.officialMenus,
|
|
5085
|
+
visible: props.visible,
|
|
5086
|
+
forge: props.forge,
|
|
5050
5087
|
addToConversation,
|
|
5051
5088
|
svn: svnStatus,
|
|
5052
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
|
})
|
package/src/pending/capture.ts
CHANGED
|
@@ -2,15 +2,32 @@
|
|
|
2
2
|
* 待审差异采集:环绕一次文件写入读取真实前后内容,只有内容确实变化才落库。
|
|
3
3
|
* @author ddj 2026年10月29号
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import { resolveTarget } from '../store.js'
|
|
5
|
+
import { cwdOfSession } from '../store.js'
|
|
7
6
|
import type { Ctx } from '../store.js'
|
|
8
7
|
import { log } from '../log.js'
|
|
8
|
+
import { toolTargets, patchDiagnostics } from '../toolTargets.js'
|
|
9
|
+
import { rawText, rawReviewFile } from '../reviewEvidence.js'
|
|
9
10
|
|
|
10
11
|
/** 可被采信为待审来源的写文件工具。 */
|
|
11
|
-
const WRITE_TOOLS = ['edit', 'write', 'str_replace_editor']
|
|
12
|
+
const WRITE_TOOLS = ['edit', 'write', 'str_replace_editor', 'apply_patch']
|
|
12
13
|
/** 单文件文本上限:超过即标注 oversized,不静默截断。 */
|
|
13
14
|
const MAX_TEXT = 1024 * 1024
|
|
15
|
+
const captureGaps = new Map<string, { cwd: string; sessionId: string; message: string }>()
|
|
16
|
+
|
|
17
|
+
/** @public @author ddj 2026年11月04号 @param cwd Workspace identity. @returns Bounded runtime-only capture diagnostics. */
|
|
18
|
+
export function pendingGaps(cwd: string): string[] {
|
|
19
|
+
return [...captureGaps.values()].filter((gap) => gap.cwd === cwd).map((gap) => gap.message)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @private @author ddj 2026年11月04号 @param ctx Host scope. @param exec Original call. @param path Failed target. @param error Evidence failure. Records no writable state. */
|
|
23
|
+
function recordGap(scope: { cwd: string; sessionId: string }, exec: WriteExec, path: string, error: unknown): void {
|
|
24
|
+
const { cwd, sessionId } = scope
|
|
25
|
+
const key = JSON.stringify([cwd, sessionId, exec.id, path])
|
|
26
|
+
captureGaps.set(key, { cwd, sessionId, message: path + ': ' + String(error) })
|
|
27
|
+
const local = [...captureGaps].filter(([, gap]) => gap.cwd === cwd)
|
|
28
|
+
while (local.length > 50) captureGaps.delete(local.shift()![0])
|
|
29
|
+
while (captureGaps.size > 500) captureGaps.delete(captureGaps.keys().next().value!)
|
|
30
|
+
}
|
|
14
31
|
|
|
15
32
|
/** 一次已确认的写入。 */
|
|
16
33
|
export interface PendingWrite {
|
|
@@ -33,12 +50,14 @@ export interface PendingCapture {
|
|
|
33
50
|
interface CaptureState {
|
|
34
51
|
busy: Set<string>
|
|
35
52
|
stopped: boolean
|
|
53
|
+
disposed: Set<string>
|
|
36
54
|
}
|
|
37
55
|
|
|
38
56
|
/** 工具的写工具参数形状;只取本采集关心的字段。 */
|
|
39
57
|
interface WriteExec {
|
|
58
|
+
id?: string
|
|
40
59
|
name?: string
|
|
41
|
-
arguments?: { file_path?: unknown; path?: unknown }
|
|
60
|
+
arguments?: { file_path?: unknown; path?: unknown; input?: unknown }
|
|
42
61
|
agent?: { session?: { id?: string; cwd?: string } }
|
|
43
62
|
}
|
|
44
63
|
|
|
@@ -50,12 +69,19 @@ interface WriteExec {
|
|
|
50
69
|
* @returns 可释放的采集器
|
|
51
70
|
*/
|
|
52
71
|
export function installPending(ctx: Ctx, sink: (write: PendingWrite) => void): PendingCapture {
|
|
53
|
-
const state: CaptureState = { busy: new Set<string>(), stopped: false }
|
|
72
|
+
const state: CaptureState = { busy: new Set<string>(), stopped: false, disposed: new Set<string>() }
|
|
54
73
|
ctx.on('tools/execute', (exec: WriteExec, next: () => Promise<unknown>) => aroundTool(ctx, state, sink, exec, next))
|
|
74
|
+
ctx.on('session/disposed', (session: { id: string }) => {
|
|
75
|
+
state.disposed.add(session.id)
|
|
76
|
+
for (const [key, gap] of captureGaps) {
|
|
77
|
+
if (gap.sessionId === session.id) captureGaps.delete(key)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
55
80
|
return {
|
|
56
81
|
dispose(): void {
|
|
57
82
|
state.stopped = true
|
|
58
83
|
state.busy.clear()
|
|
84
|
+
captureGaps.clear()
|
|
59
85
|
},
|
|
60
86
|
}
|
|
61
87
|
}
|
|
@@ -68,20 +94,24 @@ export function installPending(ctx: Ctx, sink: (write: PendingWrite) => void): P
|
|
|
68
94
|
* @param exec 工具分发上下文
|
|
69
95
|
* @returns 采集计划或 null
|
|
70
96
|
*/
|
|
71
|
-
async function planOf(
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (!WRITE_TOOLS.includes(String(exec.name))) return null
|
|
76
|
-
const cwd = cwdOf(session as never)
|
|
77
|
-
if (!cwd) return null
|
|
78
|
-
const fs = ctx.get('fs')
|
|
79
|
-
const target = await resolveTarget(ctx, session as never, requested)
|
|
97
|
+
async function planOf(scope: { cwd: string; sessionId: string; fs: any }, state: CaptureState, requested: string): Promise<CapturePlan | null> {
|
|
98
|
+
const { cwd, sessionId, fs } = scope
|
|
99
|
+
if (state.stopped || state.disposed.has(sessionId)) return null
|
|
100
|
+
const target = await fs.resolve(requested, { cwd })
|
|
80
101
|
const identity = fs.processPath(target)
|
|
81
|
-
if (typeof identity !== 'string' || !identity || state.busy.has(identity)) return null
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
102
|
+
if (state.stopped || state.disposed.has(sessionId) || typeof identity !== 'string' || !identity || state.busy.has(identity)) return null
|
|
103
|
+
state.busy.add(identity)
|
|
104
|
+
try {
|
|
105
|
+
const before = await readText(fs, target)
|
|
106
|
+
if (state.stopped || state.disposed.has(sessionId) || (before !== null && before.length > MAX_TEXT)) {
|
|
107
|
+
state.busy.delete(identity)
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
return { cwd: String(cwd), sessionId, path: requested, identity, before, target, fs }
|
|
111
|
+
} catch (error) {
|
|
112
|
+
state.busy.delete(identity)
|
|
113
|
+
throw error
|
|
114
|
+
}
|
|
85
115
|
}
|
|
86
116
|
|
|
87
117
|
/** 一次采集所需的固定信息。 */
|
|
@@ -91,7 +121,8 @@ interface CapturePlan {
|
|
|
91
121
|
path: string
|
|
92
122
|
identity: string
|
|
93
123
|
before: string | null
|
|
94
|
-
|
|
124
|
+
target: unknown
|
|
125
|
+
fs: any
|
|
95
126
|
}
|
|
96
127
|
|
|
97
128
|
/**
|
|
@@ -108,58 +139,76 @@ async function aroundTool(
|
|
|
108
139
|
ctx: Ctx, state: CaptureState, sink: (write: PendingWrite) => void,
|
|
109
140
|
exec: WriteExec, next: () => Promise<unknown>,
|
|
110
141
|
): Promise<unknown> {
|
|
111
|
-
|
|
142
|
+
if (state.stopped || !WRITE_TOOLS.includes(String(exec.name))) return next()
|
|
143
|
+
const session = exec.agent?.session
|
|
144
|
+
const cwd = session ? cwdOfSession(ctx, session as never) : null
|
|
145
|
+
if (!session?.id || !cwd) return next()
|
|
146
|
+
const scope = { cwd, sessionId: session.id, fs: ctx.get('fs') }
|
|
147
|
+
const plans: CapturePlan[] = []
|
|
112
148
|
try {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
149
|
+
const paths = await toolTargets(exec)
|
|
150
|
+
for (const path of paths) {
|
|
151
|
+
try {
|
|
152
|
+
if (plans.length >= 16) throw new Error('capture target budget exceeded')
|
|
153
|
+
const plan = await planOf(scope, state, path)
|
|
154
|
+
if (plan) plans.push(plan)
|
|
155
|
+
} catch (error) { if (!state.stopped && !state.disposed.has(scope.sessionId)) recordGap(scope, exec, path, error); log.debug('待审采集跳过:' + path + ': ' + String(error)) }
|
|
156
|
+
}
|
|
157
|
+
} catch (error) { if (!state.stopped && !state.disposed.has(scope.sessionId)) recordGap(scope, exec, String(exec.name), error); log.debug('待审目标不可用:' + String(error)) }
|
|
118
158
|
|
|
119
|
-
state.busy.add(plan.identity)
|
|
120
159
|
try {
|
|
121
|
-
|
|
160
|
+
const result = await next()
|
|
161
|
+
if (exec.name === 'apply_patch' && !state.stopped && !state.disposed.has(scope.sessionId)) {
|
|
162
|
+
for (const diagnostic of patchDiagnostics(result)) recordGap(scope, exec, 'apply_patch', diagnostic)
|
|
163
|
+
}
|
|
164
|
+
return result
|
|
122
165
|
} finally {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
log.debug('待审上报失败:' + String(error))
|
|
166
|
+
for (const plan of plans) {
|
|
167
|
+
try { await reportWrite(state, sink, plan) }
|
|
168
|
+
catch (error) { if (!state.stopped && !state.disposed.has(scope.sessionId)) recordGap(scope, exec, plan.path, error); log.debug('待审上报失败:' + String(error)) }
|
|
169
|
+
finally { state.busy.delete(plan.identity) }
|
|
128
170
|
}
|
|
129
171
|
}
|
|
130
172
|
}
|
|
131
173
|
|
|
132
174
|
/**
|
|
133
|
-
* @author ddj 2026年
|
|
134
|
-
* @description:
|
|
135
|
-
* @param
|
|
175
|
+
* @author ddj 2026年11月04号
|
|
176
|
+
* @description: 固定目标写后读回;内容一致不产生条目,卸载后不再上报。
|
|
177
|
+
* @param state 采集生命周期状态
|
|
136
178
|
* @param sink 已确认写入的接收方
|
|
137
179
|
* @param plan 采集计划
|
|
138
180
|
* @returns 无;读回失败只记日志
|
|
139
181
|
*/
|
|
140
|
-
function reportWrite(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
182
|
+
async function reportWrite(state: CaptureState, sink: (write: PendingWrite) => void, plan: CapturePlan): Promise<void> {
|
|
183
|
+
if (state.stopped || state.disposed.has(plan.sessionId)) return
|
|
184
|
+
// 目录、provider 和目标均在写前冻结,切目录或卸载不能将内容串到其他工作区。
|
|
185
|
+
const after = await readText(plan.fs, plan.target)
|
|
186
|
+
if (state.stopped || state.disposed.has(plan.sessionId)) return
|
|
187
|
+
if (after !== null && after.length > MAX_TEXT) throw new Error('capture text budget exceeded')
|
|
188
|
+
for (const [key, gap] of captureGaps) {
|
|
189
|
+
const identity = JSON.parse(key) as unknown[]
|
|
190
|
+
if (gap.cwd === plan.cwd && gap.sessionId === plan.sessionId && identity[3] === plan.path) captureGaps.delete(key)
|
|
191
|
+
}
|
|
192
|
+
if (plan.before === after) return
|
|
193
|
+
sink({ cwd: plan.cwd, sessionId: plan.sessionId, path: plan.path, identity: plan.identity, before: plan.before, after, now: Date.now() })
|
|
148
194
|
}
|
|
149
195
|
|
|
150
196
|
/**
|
|
151
197
|
* @author ddj 2026年10月29号
|
|
152
|
-
* @description:
|
|
198
|
+
* @description: 读取全文;只有明确缺失返回 null,不可读或超限错误保留失败语义。
|
|
153
199
|
* @param fs 文件系统
|
|
154
200
|
* @param target 目标
|
|
155
201
|
* @returns 文本或 null
|
|
156
202
|
*/
|
|
157
|
-
export async function readText(fs: any, target: any): Promise<string | null> {
|
|
203
|
+
export async function readText(fs: any, target: any, signal?: AbortSignal): Promise<string | null> {
|
|
158
204
|
try {
|
|
159
|
-
|
|
205
|
+
if (typeof fs.readBytes === 'function') {
|
|
206
|
+
return rawText(rawReviewFile(await fs.readBytes(target, signal, MAX_TEXT)))
|
|
207
|
+
}
|
|
208
|
+
throw new Error('Raw byte capture unavailable')
|
|
160
209
|
} catch (error) {
|
|
161
210
|
const code = (error as { code?: string }).code
|
|
162
|
-
if (code === 'ENOENT' || code === 'FS_NOT_FOUND'
|
|
211
|
+
if (code === 'ENOENT' || code === 'FS_NOT_FOUND') return null
|
|
163
212
|
throw error
|
|
164
213
|
}
|
|
165
214
|
}
|
package/src/pending/store.ts
CHANGED
|
@@ -5,10 +5,9 @@
|
|
|
5
5
|
import type { PendingDecide, PendingDetail, PendingHistoryMove, PendingItem, PendingPage } from '../shared/pendingReview.js'
|
|
6
6
|
import type { ReviewBlockState } from '../shared/reviewRegions.js'
|
|
7
7
|
import { reviewBlocks } from '../shared/reviewRegions.js'
|
|
8
|
-
import {
|
|
9
|
-
import { resolve, sep } from 'node:path'
|
|
8
|
+
import { addedFiles } from '../reviewDb.js'
|
|
10
9
|
import type { PendingWrite } from './capture.js'
|
|
11
|
-
import { readText } from './capture.js'
|
|
10
|
+
import { readText, pendingGaps } from './capture.js'
|
|
12
11
|
import type { ShellLike, VcsImportOptions } from './vcs.js'
|
|
13
12
|
import { importVcsChanges } from './vcs.js'
|
|
14
13
|
import type { DecideOutcome, WriteResult } from './decide.js'
|
|
@@ -18,7 +17,7 @@ import type { PendingEntry } from './kernel.js'
|
|
|
18
17
|
import type { PendingFlags, StoredEntry } from './persist.js'
|
|
19
18
|
import type { KeepRecord } from './persist.js'
|
|
20
19
|
import {
|
|
21
|
-
dropEntry, earliestUndone, latestApplied, loadBlocks, loadEntries, pushHistory,
|
|
20
|
+
dropEntry, earliestUndone, latestApplied, loadBlocks, loadEntries, pendingDb, pushHistory,
|
|
22
21
|
readOp, saveBlocks, saveDecisions, saveEntry, setHistoryState, writeOp,
|
|
23
22
|
} from './persist.js'
|
|
24
23
|
|
|
@@ -121,19 +120,43 @@ export function itemOf(stored: StoredEntry, baselineEmpty = false): PendingItem
|
|
|
121
120
|
return item
|
|
122
121
|
}
|
|
123
122
|
|
|
123
|
+
/**
|
|
124
|
+
* @private
|
|
125
|
+
* @author ddj 2026年11月04号
|
|
126
|
+
* @description 从条目来源判断创建基线;任何决定历史都阻止展示层复活已推进的旧基线。
|
|
127
|
+
* @param cwd 工作区
|
|
128
|
+
* @param stored 累计条目
|
|
129
|
+
* @param sourceAdds 单次读取内的来源创建集合缓存
|
|
130
|
+
* @returns 是否将展示基线置空
|
|
131
|
+
*/
|
|
132
|
+
function emptyBaseOf(cwd: string, stored: StoredEntry, sourceAdds = new Map<string, Set<string>>()): boolean {
|
|
133
|
+
if (isSettled(stored.entry)) return false
|
|
134
|
+
const decided = pendingDb(cwd).prepare('SELECT 1 FROM pending_history WHERE entry_id=? LIMIT 1').get(stored.entry.id)
|
|
135
|
+
if (decided) return false
|
|
136
|
+
for (const sourceId of stored.entry.sessionIds) {
|
|
137
|
+
let createdPaths = sourceAdds.get(sourceId)
|
|
138
|
+
if (!createdPaths) {
|
|
139
|
+
createdPaths = addedFiles(cwd, sourceId)
|
|
140
|
+
sourceAdds.set(sourceId, createdPaths)
|
|
141
|
+
}
|
|
142
|
+
if (createdPaths.has(stored.entry.id)) return true
|
|
143
|
+
}
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
|
|
124
147
|
/**
|
|
125
148
|
* @author ddj 2026年10月29号
|
|
126
|
-
* @description:
|
|
149
|
+
* @description: 读工作区全部待审条目,按更新时间升序;来源会话仅用于追溯。
|
|
127
150
|
* @param cwd 工作区
|
|
128
|
-
* @param
|
|
129
|
-
* @param wholeAdd 本次会话新建的文件身份集合,命中时基线置空
|
|
151
|
+
* @param _sessionId 兼容旧调用签名,不再用于可见性过滤
|
|
130
152
|
* @returns 列表
|
|
131
153
|
*/
|
|
132
|
-
export function loadPage(cwd: string,
|
|
154
|
+
export function loadPage(cwd: string, _sessionId?: string): PendingPage {
|
|
133
155
|
const all = loadEntries(cwd)
|
|
134
|
-
const visible =
|
|
135
|
-
const
|
|
136
|
-
const
|
|
156
|
+
const visible = all
|
|
157
|
+
const sourceAdds = new Map<string, Set<string>>()
|
|
158
|
+
const items = visible.map((stored) => itemOf(stored, emptyBaseOf(cwd, stored, sourceAdds)))
|
|
159
|
+
const gaps: string[] = pendingGaps(cwd)
|
|
137
160
|
for (const stored of visible) {
|
|
138
161
|
if (stored.flags.oversized) gaps.push(`${stored.entry.path}: 文本超出上限,仅保留标记`)
|
|
139
162
|
}
|
|
@@ -151,9 +174,10 @@ export function loadPage(cwd: string, sessionId?: string, wholeAdd?: ReadonlySet
|
|
|
151
174
|
export function loadDetail(cwd: string, id: string, baselineEmpty = false): PendingDetail | null {
|
|
152
175
|
const stored = loadEntries(cwd).find((item) => item.entry.id === id)
|
|
153
176
|
if (!stored) return null
|
|
154
|
-
const
|
|
177
|
+
const emptyBase = baselineEmpty || emptyBaseOf(cwd, stored)
|
|
178
|
+
const diff = diffOf(stored.entry, emptyBase)
|
|
155
179
|
return {
|
|
156
|
-
item: itemOf(stored,
|
|
180
|
+
item: itemOf(stored, emptyBase),
|
|
157
181
|
diff,
|
|
158
182
|
blocks: blockStatesOf(stored.entry),
|
|
159
183
|
capability: capabilityOf(stored.entry, stored.flags),
|
|
@@ -230,19 +254,19 @@ export function writerFor(
|
|
|
230
254
|
} catch {
|
|
231
255
|
return { ok: false, reason: 'resolve-failed' }
|
|
232
256
|
}
|
|
233
|
-
const
|
|
234
|
-
if (
|
|
235
|
-
//
|
|
236
|
-
const recreate = allowCreate &&
|
|
237
|
-
if (!recreate &&
|
|
238
|
-
|
|
257
|
+
const snapshot = await stableRead(fs, target, signal)
|
|
258
|
+
if (!snapshot.ok) return snapshot
|
|
259
|
+
// 缺失仅允许撤销/重做重建;原子 createIfAbsent 防止检查后被其他写入抢占。
|
|
260
|
+
const recreate = allowCreate && snapshot.content === null
|
|
261
|
+
if (!recreate && snapshot.content !== expected) return { ok: false, reason: 'diverged' }
|
|
262
|
+
if (snapshot.content === null && !recreate) return { ok: false, reason: 'missing' }
|
|
263
|
+
const intent = recreate
|
|
264
|
+
? { kind: 'createIfAbsent' }
|
|
265
|
+
: { kind: 'replaceIfVersion', version: snapshot.version }
|
|
239
266
|
try {
|
|
240
|
-
|
|
241
|
-
const written = await fs.writeText(
|
|
242
|
-
target, content ?? '', currentStat ? { kind: 'replaceIfVersion', version: currentStat.version } : undefined, signal, policy,
|
|
243
|
-
)
|
|
267
|
+
const written = await fs.writeText(target, content ?? '', intent, signal, policy)
|
|
244
268
|
const readBack = await readText(fs, target)
|
|
245
|
-
if (
|
|
269
|
+
if (content !== readBack) return { ok: false, reason: 'read-back-mismatch' }
|
|
246
270
|
if (content === null && readBack !== null) return { ok: false, reason: 'delete-failed' }
|
|
247
271
|
void written
|
|
248
272
|
return { ok: true, content }
|
|
@@ -274,7 +298,7 @@ export async function applyDecide(
|
|
|
274
298
|
blocks: beforeBlocks,
|
|
275
299
|
now: Date.now(),
|
|
276
300
|
write: (content) => {
|
|
277
|
-
if (content === null) return deleteTarget(fs, cwd, stored.entry.id, signal)
|
|
301
|
+
if (content === null) return deleteTarget(fs, cwd, stored.entry.id, stored.entry.newText, signal, policy)
|
|
278
302
|
return writerFor(fs, cwd, stored.entry.id, stored.entry.newText, signal, policy)(content)
|
|
279
303
|
},
|
|
280
304
|
})
|
|
@@ -445,7 +469,7 @@ export async function undoDecide(
|
|
|
445
469
|
async function writeBack(
|
|
446
470
|
fs: any, cwd: string, stored: StoredEntry, target: HistoryState, signal?: AbortSignal, policy?: unknown,
|
|
447
471
|
): Promise<WriteResult> {
|
|
448
|
-
if (target.flags.missing) return deleteTarget(fs, cwd, stored.entry.id, signal)
|
|
472
|
+
if (target.flags.missing) return deleteTarget(fs, cwd, stored.entry.id, stored.entry.newText, signal, policy)
|
|
449
473
|
const write = writerFor(fs, cwd, stored.entry.id, stored.entry.newText, signal, policy, true)
|
|
450
474
|
return write(target.entry.newText)
|
|
451
475
|
}
|
|
@@ -480,48 +504,62 @@ function parseSnapshot(text: string): HistorySnapshot | null {
|
|
|
480
504
|
}
|
|
481
505
|
}
|
|
482
506
|
|
|
507
|
+
/** 稳定快照;缺失与读取失败严格区分,不允许省略现存文件的版本。 */
|
|
508
|
+
type StableFile = { ok: true; content: string | null; version?: string } | { ok: false; reason: string }
|
|
509
|
+
|
|
483
510
|
/**
|
|
484
|
-
* @
|
|
485
|
-
* @
|
|
486
|
-
* @
|
|
487
|
-
* @param
|
|
488
|
-
* @param
|
|
511
|
+
* @private
|
|
512
|
+
* @author ddj 2026年11月04号
|
|
513
|
+
* @description 版本—内容—版本读取,防止将新版本令牌与旧内容拼成写入依据。
|
|
514
|
+
* @param fs 文件提供方
|
|
515
|
+
* @param target 已解析目标
|
|
489
516
|
* @param signal 取消信号
|
|
490
|
-
* @returns
|
|
517
|
+
* @returns 稳定快照或拒绝原因
|
|
491
518
|
*/
|
|
492
|
-
async function
|
|
493
|
-
let target: any
|
|
494
|
-
try {
|
|
495
|
-
target = await fs.resolve(identity, { cwd, signal })
|
|
496
|
-
} catch {
|
|
497
|
-
return { ok: false, reason: 'resolve-failed' }
|
|
498
|
-
}
|
|
499
|
-
const currentStat = await fs.stat(target, signal).catch(() => undefined)
|
|
500
|
-
if (!currentStat) return { ok: true, content: null }
|
|
501
|
-
const absolute = String(fs.processPath(target))
|
|
502
|
-
if (!insideWorkspace(cwd, absolute)) return { ok: false, reason: 'outside-workspace' }
|
|
519
|
+
async function stableRead(fs: any, target: any, signal?: AbortSignal): Promise<StableFile> {
|
|
503
520
|
try {
|
|
504
|
-
await
|
|
521
|
+
const before = await fs.stat(target, signal)
|
|
522
|
+
if (!before) return { ok: true, content: null }
|
|
523
|
+
if (before.type !== 'file' || !before.version) return { ok: false, reason: 'unguarded-file' }
|
|
524
|
+
const content = await readText(fs, target, signal)
|
|
525
|
+
if (content === null) return { ok: false, reason: 'revision-mismatch' }
|
|
526
|
+
const after = await fs.stat(target, signal)
|
|
527
|
+
if (after?.type !== 'file' || after.version !== before.version) return { ok: false, reason: 'revision-mismatch' }
|
|
528
|
+
return { ok: true, content, version: before.version }
|
|
505
529
|
} catch (error) {
|
|
506
530
|
return { ok: false, reason: reasonOf(error) }
|
|
507
531
|
}
|
|
508
|
-
const after = await fs.stat(target, signal).catch(() => undefined)
|
|
509
|
-
return after ? { ok: false, reason: 'delete-failed' } : { ok: true, content: null }
|
|
510
532
|
}
|
|
511
533
|
|
|
512
534
|
/**
|
|
513
|
-
* @
|
|
514
|
-
* @
|
|
515
|
-
* @
|
|
516
|
-
* @param
|
|
517
|
-
* @
|
|
535
|
+
* @private
|
|
536
|
+
* @author ddj 2026年11月04号
|
|
537
|
+
* @description 通过 provider 受控删除,保留会话策略与原子版本校验;旧 Host 不回落直删。
|
|
538
|
+
* @param fs 文件提供方
|
|
539
|
+
* @param cwd 工作目录
|
|
540
|
+
* @param identity 目标身份
|
|
541
|
+
* @param expected 应有内容
|
|
542
|
+
* @param signal 取消信号
|
|
543
|
+
* @param policy 会话沙箱策略
|
|
544
|
+
* @returns 删除结果;结果不明不提交决定
|
|
518
545
|
*/
|
|
519
|
-
function
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
546
|
+
async function deleteTarget(
|
|
547
|
+
fs: any, cwd: string, identity: string, expected: string, signal?: AbortSignal, policy?: unknown,
|
|
548
|
+
): Promise<WriteResult> {
|
|
549
|
+
if (typeof fs.deleteFile !== 'function') return { ok: false, reason: 'delete-unavailable' }
|
|
550
|
+
try {
|
|
551
|
+
const target = await fs.resolve(identity, { cwd, signal })
|
|
552
|
+
const snapshot = await stableRead(fs, target, signal)
|
|
553
|
+
if (!snapshot.ok) return snapshot
|
|
554
|
+
if (snapshot.content === null) return { ok: true, content: null }
|
|
555
|
+
if (snapshot.content !== expected) return { ok: false, reason: 'diverged' }
|
|
556
|
+
|
|
557
|
+
await fs.deleteFile(target, { version: snapshot.version }, signal, policy)
|
|
558
|
+
const after = await fs.stat(target, signal)
|
|
559
|
+
return after ? { ok: false, reason: 'delete-failed' } : { ok: true, content: null }
|
|
560
|
+
} catch (error) {
|
|
561
|
+
return { ok: false, reason: reasonOf(error) }
|
|
562
|
+
}
|
|
525
563
|
}
|
|
526
564
|
|
|
527
565
|
/**
|