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
package/src/pendingCompat.ts
CHANGED
|
@@ -9,11 +9,9 @@ import { pathKeyFor } from './shared/pathKey.js'
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* 进程内"已被待审接管"的路径集合;旧写入口守卫只读它,绝不碰数据库。
|
|
12
|
-
*
|
|
13
|
-
* 无会话归属的登记落在 workspace 集合(兼容只有工作区+路径的旧用法)。
|
|
12
|
+
* 按工作区登记,来源会话仅作追溯;任一对话重建或注销都会更新同一份集合。
|
|
14
13
|
*/
|
|
15
|
-
const
|
|
16
|
-
const coveredWorkspace = new Set<string>()
|
|
14
|
+
const coveredWorkspace = new Map<string, Map<string, string | null>>()
|
|
17
15
|
|
|
18
16
|
/** 待审条目在面板列表里的 id 前缀,用于把请求路由回待审内核。 */
|
|
19
17
|
export { PENDING_PREFIX } from './shared/pendingReview.js'
|
|
@@ -154,16 +152,18 @@ function coverKey(cwd: string, path: string): string {
|
|
|
154
152
|
}
|
|
155
153
|
|
|
156
154
|
/**
|
|
157
|
-
* @
|
|
158
|
-
* @
|
|
159
|
-
* @
|
|
160
|
-
* @
|
|
155
|
+
* @private
|
|
156
|
+
* @author ddj 2026年11月04号
|
|
157
|
+
* @description: 取工作区唯一接管集合;来源对话仅保留为诊断信息。
|
|
158
|
+
* @param cwd 工作区路径
|
|
159
|
+
* @returns 路径到登记来源的映射
|
|
161
160
|
*/
|
|
162
|
-
function
|
|
163
|
-
const
|
|
161
|
+
function workspaceCovers(cwd: string): Map<string, string | null> {
|
|
162
|
+
const workspaceKey = pathKey(cwd)
|
|
163
|
+
const existing = coveredWorkspace.get(workspaceKey)
|
|
164
164
|
if (existing) return existing
|
|
165
|
-
const created = new
|
|
166
|
-
|
|
165
|
+
const created = new Map<string, string | null>()
|
|
166
|
+
coveredWorkspace.set(workspaceKey, created)
|
|
167
167
|
return created
|
|
168
168
|
}
|
|
169
169
|
|
|
@@ -178,25 +178,25 @@ function sessionCovers(sessionId: string): Set<string> {
|
|
|
178
178
|
*/
|
|
179
179
|
export function trackPendingCover(cwd: string, path: string, covered: boolean, sessionId?: string): void {
|
|
180
180
|
const key = coverKey(cwd, path)
|
|
181
|
-
const target =
|
|
182
|
-
if (covered) target.
|
|
181
|
+
const target = workspaceCovers(cwd)
|
|
182
|
+
if (covered) target.set(key, sessionId ?? null)
|
|
183
183
|
else target.delete(key)
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
/**
|
|
187
187
|
* @author ddj 2026年11月3号
|
|
188
|
-
* @description:
|
|
188
|
+
* @description: 用完整列表重建工作区接管集合,清除任何对话留下的过期登记。
|
|
189
189
|
* @param cwd 工作区
|
|
190
190
|
* @param sessionId 会话 id
|
|
191
191
|
* @param paths 本会话当前可见的待审条目路径
|
|
192
192
|
* @returns 无
|
|
193
193
|
*/
|
|
194
194
|
export function setPendingCovers(cwd: string, sessionId: string, paths: readonly string[]): void {
|
|
195
|
-
const next = new
|
|
195
|
+
const next = new Map<string, string | null>()
|
|
196
196
|
for (const path of paths) {
|
|
197
|
-
if (path) next.
|
|
197
|
+
if (path) next.set(coverKey(cwd, path), sessionId)
|
|
198
198
|
}
|
|
199
|
-
|
|
199
|
+
coveredWorkspace.set(pathKey(cwd), next)
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
/**
|
|
@@ -208,16 +208,13 @@ export function setPendingCovers(cwd: string, sessionId: string, paths: readonly
|
|
|
208
208
|
*/
|
|
209
209
|
export function pendingCoverOwner(cwd: string, path: string): string | null {
|
|
210
210
|
const key = coverKey(cwd, path)
|
|
211
|
-
|
|
212
|
-
if (set.has(key)) return sessionId
|
|
213
|
-
}
|
|
214
|
-
return null
|
|
211
|
+
return coveredWorkspace.get(pathKey(cwd))?.get(key) ?? null
|
|
215
212
|
}
|
|
216
213
|
|
|
217
214
|
/**
|
|
218
215
|
* @author ddj 2026年10月29号
|
|
219
216
|
* @description: 判断某路径是否已由待审投影接管;仅读进程内集合,不产生任何 IO。
|
|
220
|
-
*
|
|
217
|
+
* 所有对话按同一工作区集合判定,兼容参数不影响可见范围。
|
|
221
218
|
* @param cwd 工作区
|
|
222
219
|
* @param path 请求路径
|
|
223
220
|
* @param sessionId 会话 id;缺省按并集判定
|
|
@@ -225,12 +222,7 @@ export function pendingCoverOwner(cwd: string, path: string): string | null {
|
|
|
225
222
|
*/
|
|
226
223
|
export function pendingCovers(cwd: string, path: string, sessionId?: string): boolean {
|
|
227
224
|
const key = coverKey(cwd, path)
|
|
228
|
-
|
|
229
|
-
if (sessionId) return coveredBySession.get(sessionId)?.has(key) === true
|
|
230
|
-
for (const set of coveredBySession.values()) {
|
|
231
|
-
if (set.has(key)) return true
|
|
232
|
-
}
|
|
233
|
-
return false
|
|
225
|
+
return coveredWorkspace.get(pathKey(cwd))?.has(key) === true
|
|
234
226
|
}
|
|
235
227
|
|
|
236
228
|
/**
|
package/src/pendingRpc.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { readText } from './pending/capture.js'
|
|
|
8
8
|
import { pendingRawId, setPendingCovers, trackPendingCover } from './pendingCompat.js'
|
|
9
9
|
import { loadEntries, saveEntry } from './pending/persist.js'
|
|
10
10
|
import { applyDecide, importFromVcs, itemOf, loadDetail, loadPage, refreshed, removeEntry, undoDecide } from './pending/store.js'
|
|
11
|
-
import { addedFiles } from './reviewDb.js'
|
|
12
11
|
import type { ShellLike } from './pending/vcs.js'
|
|
13
12
|
import type { Ctx } from './store.js'
|
|
14
13
|
import { policyOf } from './store.js'
|
|
@@ -83,16 +82,16 @@ function readHandlers(kit: ScopeKit): Partial<RpcHandlerMap> {
|
|
|
83
82
|
return {
|
|
84
83
|
'edrv.pending.list': async (args: PendingRequest) => {
|
|
85
84
|
const cwd = await kit.read(args.sessionId)
|
|
86
|
-
//
|
|
87
|
-
const page = loadPage(cwd, args.sessionId
|
|
88
|
-
//
|
|
85
|
+
// 与统一审查入口共用来源基线,不按查看者对话切分。
|
|
86
|
+
const page = loadPage(cwd, args.sessionId)
|
|
87
|
+
// 完整工作区列表重建唯一接管集合,清除所有来源的过期登记。
|
|
89
88
|
setPendingCovers(cwd, args.sessionId, page.items.map((item) => item.path))
|
|
90
89
|
return { ok: true, page }
|
|
91
90
|
},
|
|
92
91
|
'edrv.pending.detail': async (args: PendingRequest & { id: string }) => {
|
|
93
92
|
const cwd = await kit.read(args.sessionId)
|
|
94
93
|
const rawId = pendingRawId(args.id)
|
|
95
|
-
const detail = loadDetail(cwd, rawId
|
|
94
|
+
const detail = loadDetail(cwd, rawId)
|
|
96
95
|
return detail ? { ok: true, detail } : { ok: false, error: '待审条目不存在' }
|
|
97
96
|
},
|
|
98
97
|
}
|
|
@@ -124,7 +123,7 @@ function decideHandlers(kit: ScopeKit): Partial<RpcHandlerMap> {
|
|
|
124
123
|
const id = pendingRawId(args.id)
|
|
125
124
|
const path = loadEntries(cwd).find((stored) => stored.entry.id === id)?.entry.path
|
|
126
125
|
const removed = removeEntry(cwd, id, args.revision)
|
|
127
|
-
//
|
|
126
|
+
// 移除后注销工作区唯一登记,其他对话不留过期副本。
|
|
128
127
|
if (removed && path) trackPendingCover(cwd, path, false, args.sessionId)
|
|
129
128
|
return { ok: true, removed }
|
|
130
129
|
},
|
package/src/reviewCapture.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { rawReviewFile, evidenceBlocks } from './reviewEvidence.js'
|
|
|
3
3
|
import type { RawReviewFile } from './reviewEvidence.js'
|
|
4
4
|
import type { ReviewSnapshot } from './shared/review.js'
|
|
5
5
|
import { reviewDb } from './reviewDb.js'
|
|
6
|
-
import {
|
|
6
|
+
import { cwdOfSession } from './store.js'
|
|
7
|
+
import { toolTargets } from './toolTargets.js'
|
|
7
8
|
import type { Ctx } from './store.js'
|
|
8
9
|
import { log } from './log.js'
|
|
9
10
|
|
|
@@ -18,12 +19,19 @@ export function installEvidence(ctx: Ctx): EvidenceCapture {
|
|
|
18
19
|
const chains = new Map<string, Map<string, ByteChain>>()
|
|
19
20
|
const staged = new Map<string, Map<string, ByteChain>>()
|
|
20
21
|
let stopped = false
|
|
22
|
+
const disposed = new Set<string>()
|
|
21
23
|
ctx.on('tools/execute', aroundTool)
|
|
22
24
|
ctx.on('session/event', onEvent)
|
|
23
25
|
ctx.on('session/disposed', onDisposed)
|
|
24
26
|
|
|
25
27
|
/** @private @author ddj 2026年10月27号 @param session Disposed owner. Drops optional in-memory evidence, never persisted official facts. */
|
|
26
|
-
function onDisposed(session: { id: string }): void {
|
|
28
|
+
function onDisposed(session: { id: string }): void {
|
|
29
|
+
disposed.add(session.id)
|
|
30
|
+
chains.delete(session.id)
|
|
31
|
+
for (const key of staged.keys()) {
|
|
32
|
+
if ((JSON.parse(key) as unknown[])[0] === session.id) staged.delete(key)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
27
35
|
|
|
28
36
|
/** @private @author ddj 2026年10月27号 @returns Current bounded capture footprint, excluding transient filesystem reads. */
|
|
29
37
|
function footprint(): number {
|
|
@@ -39,34 +47,67 @@ export function installEvidence(ctx: Ctx): EvidenceCapture {
|
|
|
39
47
|
/** @private @author ddj 2026年10月27号 @param exec Allowed tool dispatch. @param next Original dispatch. @returns Original result unchanged, regardless of capture failure. */
|
|
40
48
|
async function aroundTool(exec: any, next: () => Promise<any>): Promise<any> {
|
|
41
49
|
const session = exec.agent?.session
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
let fs: any
|
|
45
|
-
let chain: ByteChain | undefined
|
|
46
|
-
let target: any
|
|
50
|
+
if (stopped || !session?.id) return next()
|
|
51
|
+
const captures: Array<{ fs: any; target: any; chain: ByteChain }> = []
|
|
47
52
|
try {
|
|
48
|
-
|
|
49
|
-
fs = ctx.get('fs')
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (chain.busy || chain.after.hash !== before.hash) chain.invalid = true
|
|
59
|
-
chain.busy = true
|
|
60
|
-
} catch { /* No trustworthy read means no undo evidence, not a failed user tool. */ }
|
|
53
|
+
const cwd = cwdOfSession(ctx, session)
|
|
54
|
+
const fs = ctx.get('fs')
|
|
55
|
+
const paths = await toolTargets(exec)
|
|
56
|
+
for (const path of paths) {
|
|
57
|
+
try {
|
|
58
|
+
const capture = await prepareRaw(fs, session.id, cwd ?? undefined, path, exec.signal)
|
|
59
|
+
if (capture) captures.push(capture)
|
|
60
|
+
} catch { /* A failed target never prevents independent targets. */ }
|
|
61
|
+
}
|
|
62
|
+
} catch { /* Optional parser absence must not fail the original tool. */ }
|
|
61
63
|
try { return await next() }
|
|
62
64
|
finally {
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
|
|
65
|
+
for (const capture of captures) {
|
|
66
|
+
try {
|
|
67
|
+
const after = await readRaw(capture.fs, capture.target, exec.signal)
|
|
68
|
+
if (!stopped && !disposed.has(session.id)) {
|
|
69
|
+
capture.chain.after = after
|
|
70
|
+
if (footprint() > 32 * 1024 * 1024) {
|
|
71
|
+
capture.chain.invalid = true
|
|
72
|
+
capture.chain.before = rawReviewFile(null); capture.chain.after = rawReviewFile(null)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch { capture.chain.invalid = true }
|
|
76
|
+
capture.chain.busy = false
|
|
66
77
|
}
|
|
67
78
|
}
|
|
68
79
|
}
|
|
69
80
|
|
|
81
|
+
/** @private @author ddj 2026年11月04号 @param sessionId Frozen owner. @param cwd Frozen workspace. @param path Patch target. @param signal Tool cancellation. @returns Reserved raw capture or no evidence. */
|
|
82
|
+
async function prepareRaw(fs: any, sessionId: string, cwd: string | undefined, path: string, signal?: AbortSignal) {
|
|
83
|
+
if (stopped || disposed.has(sessionId) || !cwd || (chains.size >= 16 && !chains.has(sessionId)) || footprint() >= 32 * 1024 * 1024) return null
|
|
84
|
+
const target = await fs.resolve(path, { cwd })
|
|
85
|
+
if (stopped || disposed.has(sessionId)) return null
|
|
86
|
+
const identity = fs.processPath(target)
|
|
87
|
+
let files = chains.get(sessionId)
|
|
88
|
+
if (!files) { files = new Map(); chains.set(sessionId, files) }
|
|
89
|
+
if (files.size >= 64 && !files.has(identity)) return null
|
|
90
|
+
let chain = files.get(identity)
|
|
91
|
+
if (chain?.busy) { chain.invalid = true; return null }
|
|
92
|
+
const empty = rawReviewFile(null)
|
|
93
|
+
if (!chain) {
|
|
94
|
+
chain = { before: empty, after: empty, busy: true, invalid: false }
|
|
95
|
+
files.set(identity, chain)
|
|
96
|
+
try { chain.before = await readRaw(fs, target, signal); chain.after = chain.before }
|
|
97
|
+
catch (error) { chain.invalid = true; chain.busy = false; throw error }
|
|
98
|
+
} else {
|
|
99
|
+
chain.busy = true
|
|
100
|
+
try {
|
|
101
|
+
const before = await readRaw(fs, target, signal)
|
|
102
|
+
if (chain.after.hash !== before.hash) chain.invalid = true
|
|
103
|
+
} catch (error) { chain.invalid = true; chain.busy = false; throw error }
|
|
104
|
+
}
|
|
105
|
+
if (stopped || disposed.has(sessionId) || footprint() > 32 * 1024 * 1024) {
|
|
106
|
+
chain.invalid = true; chain.busy = false; files.delete(identity); return null
|
|
107
|
+
}
|
|
108
|
+
return { fs, target, chain }
|
|
109
|
+
}
|
|
110
|
+
|
|
70
111
|
/** @private @author ddj 2026年10月27号 @param session Source session. @param event Published official turn. Freezes the chain before the next turn can mutate it. */
|
|
71
112
|
function onEvent(session: any, event: any): void {
|
|
72
113
|
if (stopped || event.type !== 'workspace/changes') return
|
package/src/reviewEvidence.ts
CHANGED
|
@@ -16,7 +16,7 @@ export function rawReviewFile(bytes: Uint8Array | null): RawReviewFile {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** @private @author ddj 2026年10月27号 @param file Persisted raw image. @returns UTF-8 storage text preserving BOM and terminators. @throws Binary, invalid encoding or corrupt hash. */
|
|
19
|
-
function rawText(file: RawReviewFile): string {
|
|
19
|
+
export function rawText(file: RawReviewFile): string {
|
|
20
20
|
const bytes = Buffer.from(file.bytes, 'base64')
|
|
21
21
|
if (bytes.length > 8 * 1024 * 1024 || bytes.includes(0)) throw new Error('回滚证据不是受支持的 UTF-8 文本')
|
|
22
22
|
if (rawReviewFile(file.present ? bytes : null).hash !== file.hash) throw new Error('回滚证据哈希不匹配')
|
package/src/reviewRpc.ts
CHANGED
|
@@ -3,13 +3,13 @@ import { createHash } from 'node:crypto'
|
|
|
3
3
|
import type { RpcHandlerMap } from './shared/rpc.js'
|
|
4
4
|
import type { KeepAllItem, KeepAllResult, ReviewDetail, ReviewItem, ReviewPage } from './shared/review.js'
|
|
5
5
|
import type { PendingPage } from './shared/pendingReview.js'
|
|
6
|
-
import {
|
|
6
|
+
import { decideReview, keepOfficialAll, reviewDetail, reviewPage } from './reviewDb.js'
|
|
7
7
|
import { undoReview } from './reviewUndo.js'
|
|
8
8
|
import { applyDecide, keepAll, loadDetail, loadPage } from './pending/store.js'
|
|
9
9
|
import { PENDING_PREFIX, mergedPage, pendingDetailOf, pendingItemOf, pendingSamePath } from './pendingCompat.js'
|
|
10
10
|
import { bucketOf, resolveScope, sessionOf } from './registry.js'
|
|
11
11
|
import type { Registry } from './registry.js'
|
|
12
|
-
import { resolveTargetIn } from './store.js'
|
|
12
|
+
import { policyOf, resolveTargetIn } from './store.js'
|
|
13
13
|
import type { Ctx } from './store.js'
|
|
14
14
|
import { log } from './log.js'
|
|
15
15
|
|
|
@@ -154,9 +154,8 @@ export function reviewHandlers(ctx: Ctx, registry: Registry): Partial<RpcHandler
|
|
|
154
154
|
*/
|
|
155
155
|
function pendingPageOf(cwd: string, sessionId: string): PendingPage {
|
|
156
156
|
try {
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
const page = loadPage(cwd, sessionId, addedFiles(cwd, sessionId))
|
|
157
|
+
// 基线由累计条目来源及决定历史统一解析,不依赖当前查看者对话。
|
|
158
|
+
const page = loadPage(cwd, sessionId)
|
|
160
159
|
// 已结算(老文本与新文本一致)的投影没有净变化可审:留着会顶掉同路径的官方待审记录,
|
|
161
160
|
// 让差异栏选中一条 Keep 不可用的条目,表现为「按钮亮着却点不动」。
|
|
162
161
|
return page.items.some((item) => item.settled === true)
|
|
@@ -279,7 +278,7 @@ export function reviewHandlers(ctx: Ctx, registry: Registry): Partial<RpcHandler
|
|
|
279
278
|
if (args.id.startsWith(PENDING_PREFIX)) {
|
|
280
279
|
const entryId = args.id.slice(PENDING_PREFIX.length)
|
|
281
280
|
// 详情侧与列表侧必须同一口径:整文件新增的文件用空基线渲染,差异块才是全绿一整块
|
|
282
|
-
const detail = loadDetail(cwd, entryId
|
|
281
|
+
const detail = loadDetail(cwd, entryId)
|
|
283
282
|
if (!detail) return { ok: false, error: '待审条目不存在' }
|
|
284
283
|
|
|
285
284
|
const mapped = pendingDetailOf(detail, new Date(detail.item.updatedAt).toISOString(), cwd)
|
|
@@ -314,7 +313,7 @@ export function reviewHandlers(ctx: Ctx, registry: Registry): Partial<RpcHandler
|
|
|
314
313
|
decision: args.decision,
|
|
315
314
|
scope: args.scope === 'hunk' ? 'block' : 'file',
|
|
316
315
|
...(args.blockId === undefined ? {} : { blockId: args.blockId }),
|
|
317
|
-
})
|
|
316
|
+
}, undefined, policyOf(ctx, sessionOf(ctx, args.sessionId)))
|
|
318
317
|
if (!result.ok) return { ok: false, error: result.reason }
|
|
319
318
|
return { ok: true, revision: result.flags.revision }
|
|
320
319
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Optional Host patch target extraction; results never substitute raw preimages. @author ddj 2026年11月04号 */
|
|
2
|
+
import { loadHostModule } from './hostImport.js'
|
|
3
|
+
|
|
4
|
+
export interface ToolCall {
|
|
5
|
+
name?: string
|
|
6
|
+
id?: string
|
|
7
|
+
arguments?: { file_path?: unknown; path?: unknown; input?: unknown }
|
|
8
|
+
}
|
|
9
|
+
interface PatchTarget { path: string; moveTo?: string }
|
|
10
|
+
|
|
11
|
+
/** @public @author ddj 2026年11月04号 @param result Untrusted tool envelope. @returns Diagnostic-only status; never writable preimages. */
|
|
12
|
+
export function patchDiagnostics(result: unknown): string[] {
|
|
13
|
+
const envelope = result as { value?: unknown } | null
|
|
14
|
+
const payload = (envelope?.value ?? result) as { phase?: unknown; operations?: unknown } | null
|
|
15
|
+
if (payload?.phase !== 'commit' || !Array.isArray(payload.operations)) return ['apply_patch result unavailable or malformed']
|
|
16
|
+
const diagnostics: string[] = []
|
|
17
|
+
for (const operation of payload.operations) {
|
|
18
|
+
if (!operation || typeof operation.path !== 'string' || !['add', 'update', 'delete', 'move'].includes(operation.kind)
|
|
19
|
+
|| !['completed', 'failed', 'unknown', 'skipped'].includes(operation.status)) {
|
|
20
|
+
diagnostics.push('apply_patch operation malformed'); continue
|
|
21
|
+
}
|
|
22
|
+
if (operation.status !== 'completed') diagnostics.push(operation.path + ': ' + operation.status)
|
|
23
|
+
if (operation.changes !== undefined && (!Array.isArray(operation.changes) || operation.changes.some((change: any) =>
|
|
24
|
+
!change || typeof change.path !== 'string' || !(change.before === null || typeof change.before === 'string') || !(change.after === null || typeof change.after === 'string')))) {
|
|
25
|
+
diagnostics.push(operation.path + ': changes malformed')
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return diagnostics
|
|
29
|
+
}
|
|
30
|
+
export type PatchParser = (input: string) => PatchTarget[]
|
|
31
|
+
let parserLoad: Promise<PatchParser | null> | undefined
|
|
32
|
+
|
|
33
|
+
/** @public @author ddj 2026年11月04号 @returns Optional official parser; missing capabilities remain retryable. */
|
|
34
|
+
export async function patchParser(): Promise<PatchParser | null> {
|
|
35
|
+
if (!parserLoad) {
|
|
36
|
+
parserLoad = loadHostModule('@deepseek-ai/dsh-fs-patch').then((module) => {
|
|
37
|
+
const parser = (module as { parsePatch?: PatchParser }).parsePatch
|
|
38
|
+
return typeof parser === 'function' ? parser : null
|
|
39
|
+
}).catch(() => null)
|
|
40
|
+
}
|
|
41
|
+
const parser = await parserLoad
|
|
42
|
+
if (!parser) parserLoad = undefined
|
|
43
|
+
return parser
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @public @author ddj 2026年11月04号 @param call Tool identity and arguments. @param parser Optional injectable official parser. @returns Unique unresolved source/destination paths. @throws Unsupported parser or malformed patch. */
|
|
47
|
+
export async function toolTargets(call: ToolCall, parser?: PatchParser | null): Promise<string[]> {
|
|
48
|
+
if (['edit', 'write', 'str_replace_editor'].includes(String(call.name))) {
|
|
49
|
+
const path = call.arguments?.file_path ?? call.arguments?.path
|
|
50
|
+
return typeof path === 'string' && path ? [path] : []
|
|
51
|
+
}
|
|
52
|
+
if (call.name !== 'apply_patch') return []
|
|
53
|
+
if (typeof call.arguments?.input !== 'string') throw new Error('apply_patch input unavailable')
|
|
54
|
+
const parse = parser === undefined ? await patchParser() : parser
|
|
55
|
+
if (!parse) throw new Error('Host patch parser unavailable')
|
|
56
|
+
const paths = new Set<string>()
|
|
57
|
+
for (const operation of parse(call.arguments.input)) {
|
|
58
|
+
if (typeof operation.path !== 'string' || !operation.path) throw new Error('Invalid patch target')
|
|
59
|
+
paths.add(operation.path)
|
|
60
|
+
if (operation.moveTo) paths.add(operation.moveTo)
|
|
61
|
+
}
|
|
62
|
+
return [...paths]
|
|
63
|
+
}
|