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
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'
|
|
@@ -16,9 +15,10 @@ import { capabilityOf, decide, diffOf } from './decide.js'
|
|
|
16
15
|
import { entryFromNew, isSettled, mergeEntries } from './kernel.js'
|
|
17
16
|
import type { PendingEntry } from './kernel.js'
|
|
18
17
|
import type { PendingFlags, StoredEntry } from './persist.js'
|
|
18
|
+
import type { KeepRecord } from './persist.js'
|
|
19
19
|
import {
|
|
20
|
-
dropEntry, earliestUndone, latestApplied, loadBlocks, loadEntries, pushHistory,
|
|
21
|
-
readOp, saveBlocks, saveEntry, setHistoryState, writeOp,
|
|
20
|
+
dropEntry, earliestUndone, latestApplied, loadBlocks, loadEntries, pendingDb, pushHistory,
|
|
21
|
+
readOp, saveBlocks, saveDecisions, saveEntry, setHistoryState, writeOp,
|
|
22
22
|
} from './persist.js'
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -120,19 +120,43 @@ export function itemOf(stored: StoredEntry, baselineEmpty = false): PendingItem
|
|
|
120
120
|
return item
|
|
121
121
|
}
|
|
122
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
|
+
|
|
123
147
|
/**
|
|
124
148
|
* @author ddj 2026年10月29号
|
|
125
|
-
* @description:
|
|
149
|
+
* @description: 读工作区全部待审条目,按更新时间升序;来源会话仅用于追溯。
|
|
126
150
|
* @param cwd 工作区
|
|
127
|
-
* @param
|
|
128
|
-
* @param wholeAdd 本次会话新建的文件身份集合,命中时基线置空
|
|
151
|
+
* @param _sessionId 兼容旧调用签名,不再用于可见性过滤
|
|
129
152
|
* @returns 列表
|
|
130
153
|
*/
|
|
131
|
-
export function loadPage(cwd: string,
|
|
154
|
+
export function loadPage(cwd: string, _sessionId?: string): PendingPage {
|
|
132
155
|
const all = loadEntries(cwd)
|
|
133
|
-
const visible =
|
|
134
|
-
const
|
|
135
|
-
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)
|
|
136
160
|
for (const stored of visible) {
|
|
137
161
|
if (stored.flags.oversized) gaps.push(`${stored.entry.path}: 文本超出上限,仅保留标记`)
|
|
138
162
|
}
|
|
@@ -150,9 +174,10 @@ export function loadPage(cwd: string, sessionId?: string, wholeAdd?: ReadonlySet
|
|
|
150
174
|
export function loadDetail(cwd: string, id: string, baselineEmpty = false): PendingDetail | null {
|
|
151
175
|
const stored = loadEntries(cwd).find((item) => item.entry.id === id)
|
|
152
176
|
if (!stored) return null
|
|
153
|
-
const
|
|
177
|
+
const emptyBase = baselineEmpty || emptyBaseOf(cwd, stored)
|
|
178
|
+
const diff = diffOf(stored.entry, emptyBase)
|
|
154
179
|
return {
|
|
155
|
-
item: itemOf(stored,
|
|
180
|
+
item: itemOf(stored, emptyBase),
|
|
156
181
|
diff,
|
|
157
182
|
blocks: blockStatesOf(stored.entry),
|
|
158
183
|
capability: capabilityOf(stored.entry, stored.flags),
|
|
@@ -229,19 +254,19 @@ export function writerFor(
|
|
|
229
254
|
} catch {
|
|
230
255
|
return { ok: false, reason: 'resolve-failed' }
|
|
231
256
|
}
|
|
232
|
-
const
|
|
233
|
-
if (
|
|
234
|
-
//
|
|
235
|
-
const recreate = allowCreate &&
|
|
236
|
-
if (!recreate &&
|
|
237
|
-
|
|
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 }
|
|
238
266
|
try {
|
|
239
|
-
|
|
240
|
-
const written = await fs.writeText(
|
|
241
|
-
target, content ?? '', currentStat ? { kind: 'replaceIfVersion', version: currentStat.version } : undefined, signal, policy,
|
|
242
|
-
)
|
|
267
|
+
const written = await fs.writeText(target, content ?? '', intent, signal, policy)
|
|
243
268
|
const readBack = await readText(fs, target)
|
|
244
|
-
if (
|
|
269
|
+
if (content !== readBack) return { ok: false, reason: 'read-back-mismatch' }
|
|
245
270
|
if (content === null && readBack !== null) return { ok: false, reason: 'delete-failed' }
|
|
246
271
|
void written
|
|
247
272
|
return { ok: true, content }
|
|
@@ -273,7 +298,7 @@ export async function applyDecide(
|
|
|
273
298
|
blocks: beforeBlocks,
|
|
274
299
|
now: Date.now(),
|
|
275
300
|
write: (content) => {
|
|
276
|
-
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)
|
|
277
302
|
return writerFor(fs, cwd, stored.entry.id, stored.entry.newText, signal, policy)(content)
|
|
278
303
|
},
|
|
279
304
|
})
|
|
@@ -289,6 +314,80 @@ export async function applyDecide(
|
|
|
289
314
|
return outcome
|
|
290
315
|
}
|
|
291
316
|
|
|
317
|
+
/** 批量 Keep 的目标条目:display 只用于回报,operationId 由 host 按批次派生。 */
|
|
318
|
+
export interface PendingKeepTarget {
|
|
319
|
+
id: string
|
|
320
|
+
display: string
|
|
321
|
+
operationId: string
|
|
322
|
+
}
|
|
323
|
+
/** 批量 Keep 的结果:kept 是真正推进到保留态的条目,skipped 说明该条为什么没保留。 */
|
|
324
|
+
export interface PendingKeepOutcome {
|
|
325
|
+
kept: { id: string; path: string }[]
|
|
326
|
+
skipped: { id: string; display: string; reason: string }[]
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* @public
|
|
331
|
+
* @author ddj 2026年11月03号
|
|
332
|
+
* @description: 一次读取全表后批量保留待审条目(Keep 不写盘)。写入用刚读到的最新 revision,
|
|
333
|
+
* 客户端快照漂移不会整批中止;单条 oversized、不存在或落库时被抢先推进只跳过自己。
|
|
334
|
+
* @param cwd 工作区
|
|
335
|
+
* @param items 待保留条目
|
|
336
|
+
* @returns 保留成功与跳过的明细
|
|
337
|
+
* @throws 待审库迁移未就绪等存储级异常
|
|
338
|
+
*/
|
|
339
|
+
export async function keepAll(cwd: string, items: PendingKeepTarget[]): Promise<PendingKeepOutcome> {
|
|
340
|
+
const stored = new Map(loadEntries(cwd).map((item) => [item.entry.id, item]))
|
|
341
|
+
const now = Date.now()
|
|
342
|
+
const outcome: PendingKeepOutcome = { kept: [], skipped: [] }
|
|
343
|
+
const records: KeepRecord[] = []
|
|
344
|
+
|
|
345
|
+
for (const item of items) {
|
|
346
|
+
const before = stored.get(item.id)
|
|
347
|
+
if (!before) {
|
|
348
|
+
outcome.skipped.push({ id: item.id, display: item.display, reason: '待审条目不存在' })
|
|
349
|
+
continue
|
|
350
|
+
}
|
|
351
|
+
if (before.flags.oversized) {
|
|
352
|
+
outcome.skipped.push({ id: item.id, display: item.display, reason: '文本超出上限,不能整文件保留' })
|
|
353
|
+
continue
|
|
354
|
+
}
|
|
355
|
+
// 已经是保留态:幂等无操作,仍算保留成功
|
|
356
|
+
if (isSettled(before.entry)) {
|
|
357
|
+
outcome.kept.push({ id: item.id, path: before.entry.path })
|
|
358
|
+
continue
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const beforeBlocks = blockStatesOf(before.entry)
|
|
362
|
+
const outcomeOf = await decide({
|
|
363
|
+
request: { sessionId: before.entry.sessionId, id: item.id, revision: before.flags.revision,
|
|
364
|
+
operationId: item.operationId, decision: 'accepted', scope: 'file' },
|
|
365
|
+
entry: before.entry, flags: before.flags, blocks: beforeBlocks, now,
|
|
366
|
+
// Keep 永不写盘:真走到写回说明逻辑被改坏,宁可炸掉也不静默落盘
|
|
367
|
+
write: () => { throw new Error('批量 Keep 不写盘') },
|
|
368
|
+
})
|
|
369
|
+
if (!outcomeOf.ok) {
|
|
370
|
+
outcome.skipped.push({ id: item.id, display: item.display, reason: outcomeOf.reason })
|
|
371
|
+
continue
|
|
372
|
+
}
|
|
373
|
+
outcome.kept.push({ id: item.id, path: before.entry.path })
|
|
374
|
+
records.push({ entryId: item.id, stored: before, next: { entry: outcomeOf.entry, flags: outcomeOf.flags },
|
|
375
|
+
blocks: outcomeOf.blocks, snapshot: JSON.stringify({
|
|
376
|
+
before: { entry: before.entry, flags: before.flags, blocks: beforeBlocks },
|
|
377
|
+
after: { entry: outcomeOf.entry, flags: outcomeOf.flags, blocks: outcomeOf.blocks },
|
|
378
|
+
wrote: outcomeOf.wrote,
|
|
379
|
+
}) })
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// 落库阶段被别的决定抢先推进的条目从 kept 挪到 skipped,如实反映磁盘状态
|
|
383
|
+
const displayOf = new Map(items.map((item) => [item.id, item.display]))
|
|
384
|
+
for (const conflict of saveDecisions(cwd, records)) {
|
|
385
|
+
outcome.kept = outcome.kept.filter((entry) => entry.id !== conflict.id)
|
|
386
|
+
outcome.skipped.push({ id: conflict.id, display: displayOf.get(conflict.id) ?? conflict.id, reason: conflict.reason })
|
|
387
|
+
}
|
|
388
|
+
return outcome
|
|
389
|
+
}
|
|
390
|
+
|
|
292
391
|
/**
|
|
293
392
|
* @author ddj 2026年10月30号
|
|
294
393
|
* @description: 版本库导入:把工作副本相对基线的改动折进待审条目,已存在条目按折叠规则并入。
|
|
@@ -370,7 +469,7 @@ export async function undoDecide(
|
|
|
370
469
|
async function writeBack(
|
|
371
470
|
fs: any, cwd: string, stored: StoredEntry, target: HistoryState, signal?: AbortSignal, policy?: unknown,
|
|
372
471
|
): Promise<WriteResult> {
|
|
373
|
-
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)
|
|
374
473
|
const write = writerFor(fs, cwd, stored.entry.id, stored.entry.newText, signal, policy, true)
|
|
375
474
|
return write(target.entry.newText)
|
|
376
475
|
}
|
|
@@ -405,48 +504,62 @@ function parseSnapshot(text: string): HistorySnapshot | null {
|
|
|
405
504
|
}
|
|
406
505
|
}
|
|
407
506
|
|
|
507
|
+
/** 稳定快照;缺失与读取失败严格区分,不允许省略现存文件的版本。 */
|
|
508
|
+
type StableFile = { ok: true; content: string | null; version?: string } | { ok: false; reason: string }
|
|
509
|
+
|
|
408
510
|
/**
|
|
409
|
-
* @
|
|
410
|
-
* @
|
|
411
|
-
* @
|
|
412
|
-
* @param
|
|
413
|
-
* @param
|
|
511
|
+
* @private
|
|
512
|
+
* @author ddj 2026年11月04号
|
|
513
|
+
* @description 版本—内容—版本读取,防止将新版本令牌与旧内容拼成写入依据。
|
|
514
|
+
* @param fs 文件提供方
|
|
515
|
+
* @param target 已解析目标
|
|
414
516
|
* @param signal 取消信号
|
|
415
|
-
* @returns
|
|
517
|
+
* @returns 稳定快照或拒绝原因
|
|
416
518
|
*/
|
|
417
|
-
async function
|
|
418
|
-
let target: any
|
|
419
|
-
try {
|
|
420
|
-
target = await fs.resolve(identity, { cwd, signal })
|
|
421
|
-
} catch {
|
|
422
|
-
return { ok: false, reason: 'resolve-failed' }
|
|
423
|
-
}
|
|
424
|
-
const currentStat = await fs.stat(target, signal).catch(() => undefined)
|
|
425
|
-
if (!currentStat) return { ok: true, content: null }
|
|
426
|
-
const absolute = String(fs.processPath(target))
|
|
427
|
-
if (!insideWorkspace(cwd, absolute)) return { ok: false, reason: 'outside-workspace' }
|
|
519
|
+
async function stableRead(fs: any, target: any, signal?: AbortSignal): Promise<StableFile> {
|
|
428
520
|
try {
|
|
429
|
-
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 }
|
|
430
529
|
} catch (error) {
|
|
431
530
|
return { ok: false, reason: reasonOf(error) }
|
|
432
531
|
}
|
|
433
|
-
const after = await fs.stat(target, signal).catch(() => undefined)
|
|
434
|
-
return after ? { ok: false, reason: 'delete-failed' } : { ok: true, content: null }
|
|
435
532
|
}
|
|
436
533
|
|
|
437
534
|
/**
|
|
438
|
-
* @
|
|
439
|
-
* @
|
|
440
|
-
* @
|
|
441
|
-
* @param
|
|
442
|
-
* @
|
|
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 删除结果;结果不明不提交决定
|
|
443
545
|
*/
|
|
444
|
-
function
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
+
}
|
|
450
563
|
}
|
|
451
564
|
|
|
452
565
|
/**
|
package/src/pendingCompat.ts
CHANGED
|
@@ -7,8 +7,11 @@ import type { ReviewDetail, ReviewItem, ReviewPage } from './shared/review.js'
|
|
|
7
7
|
import type { PendingDetail, PendingItem, PendingPage } from './shared/pendingReview.js'
|
|
8
8
|
import { pathKeyFor } from './shared/pathKey.js'
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* 进程内"已被待审接管"的路径集合;旧写入口守卫只读它,绝不碰数据库。
|
|
12
|
+
* 按工作区登记,来源会话仅作追溯;任一对话重建或注销都会更新同一份集合。
|
|
13
|
+
*/
|
|
14
|
+
const coveredWorkspace = new Map<string, Map<string, string | null>>()
|
|
12
15
|
|
|
13
16
|
/** 待审条目在面板列表里的 id 前缀,用于把请求路由回待审内核。 */
|
|
14
17
|
export { PENDING_PREFIX } from './shared/pendingReview.js'
|
|
@@ -138,28 +141,88 @@ export function pendingDetailOf(detail: PendingDetail, capturedAt: string, cwd?:
|
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
/**
|
|
141
|
-
* @author ddj 2026年
|
|
144
|
+
* @author ddj 2026年11月3号
|
|
145
|
+
* @description: 覆盖登记键:工作区与路径各自归一化后再拼接,避免同一文件多种写法登记成两条。
|
|
146
|
+
* @param cwd 工作区
|
|
147
|
+
* @param path 路径
|
|
148
|
+
* @returns 覆盖登记键
|
|
149
|
+
*/
|
|
150
|
+
function coverKey(cwd: string, path: string): string {
|
|
151
|
+
return `${pathKey(cwd)}|${pathKey(path)}`
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @private
|
|
156
|
+
* @author ddj 2026年11月04号
|
|
157
|
+
* @description: 取工作区唯一接管集合;来源对话仅保留为诊断信息。
|
|
158
|
+
* @param cwd 工作区路径
|
|
159
|
+
* @returns 路径到登记来源的映射
|
|
160
|
+
*/
|
|
161
|
+
function workspaceCovers(cwd: string): Map<string, string | null> {
|
|
162
|
+
const workspaceKey = pathKey(cwd)
|
|
163
|
+
const existing = coveredWorkspace.get(workspaceKey)
|
|
164
|
+
if (existing) return existing
|
|
165
|
+
const created = new Map<string, string | null>()
|
|
166
|
+
coveredWorkspace.set(workspaceKey, created)
|
|
167
|
+
return created
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @author ddj 2026年11月3号
|
|
142
172
|
* @description: 记住/忘记"该路径由待审投影接管"。进程内集合,绝不在旧写入口上查库(避免撞上写事务锁)。
|
|
143
173
|
* @param cwd 工作区
|
|
144
174
|
* @param path 路径
|
|
145
175
|
* @param covered 是否接管
|
|
176
|
+
* @param sessionId 会话 id;缺省落到无归属的工作区集合
|
|
146
177
|
* @returns 无
|
|
147
178
|
*/
|
|
148
|
-
export function trackPendingCover(cwd: string, path: string, covered: boolean): void {
|
|
149
|
-
const key =
|
|
150
|
-
|
|
151
|
-
|
|
179
|
+
export function trackPendingCover(cwd: string, path: string, covered: boolean, sessionId?: string): void {
|
|
180
|
+
const key = coverKey(cwd, path)
|
|
181
|
+
const target = workspaceCovers(cwd)
|
|
182
|
+
if (covered) target.set(key, sessionId ?? null)
|
|
183
|
+
else target.delete(key)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @author ddj 2026年11月3号
|
|
188
|
+
* @description: 用完整列表重建工作区接管集合,清除任何对话留下的过期登记。
|
|
189
|
+
* @param cwd 工作区
|
|
190
|
+
* @param sessionId 会话 id
|
|
191
|
+
* @param paths 本会话当前可见的待审条目路径
|
|
192
|
+
* @returns 无
|
|
193
|
+
*/
|
|
194
|
+
export function setPendingCovers(cwd: string, sessionId: string, paths: readonly string[]): void {
|
|
195
|
+
const next = new Map<string, string | null>()
|
|
196
|
+
for (const path of paths) {
|
|
197
|
+
if (path) next.set(coverKey(cwd, path), sessionId)
|
|
198
|
+
}
|
|
199
|
+
coveredWorkspace.set(pathKey(cwd), next)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @author ddj 2026年11月3号
|
|
204
|
+
* @description: 找出接管某路径的会话,用于把"谁在管"写进提示文案;仅读内存,不查库。
|
|
205
|
+
* @param cwd 工作区
|
|
206
|
+
* @param path 路径
|
|
207
|
+
* @returns 首个接管会话 id;无人接管返回 null
|
|
208
|
+
*/
|
|
209
|
+
export function pendingCoverOwner(cwd: string, path: string): string | null {
|
|
210
|
+
const key = coverKey(cwd, path)
|
|
211
|
+
return coveredWorkspace.get(pathKey(cwd))?.get(key) ?? null
|
|
152
212
|
}
|
|
153
213
|
|
|
154
214
|
/**
|
|
155
215
|
* @author ddj 2026年10月29号
|
|
156
216
|
* @description: 判断某路径是否已由待审投影接管;仅读进程内集合,不产生任何 IO。
|
|
217
|
+
* 所有对话按同一工作区集合判定,兼容参数不影响可见范围。
|
|
157
218
|
* @param cwd 工作区
|
|
158
219
|
* @param path 请求路径
|
|
220
|
+
* @param sessionId 会话 id;缺省按并集判定
|
|
159
221
|
* @returns 是否已被接管
|
|
160
222
|
*/
|
|
161
|
-
export function pendingCovers(cwd: string, path: string): boolean {
|
|
162
|
-
|
|
223
|
+
export function pendingCovers(cwd: string, path: string, sessionId?: string): boolean {
|
|
224
|
+
const key = coverKey(cwd, path)
|
|
225
|
+
return coveredWorkspace.get(pathKey(cwd))?.has(key) === true
|
|
163
226
|
}
|
|
164
227
|
|
|
165
228
|
/**
|
package/src/pendingRpc.ts
CHANGED
|
@@ -5,10 +5,9 @@ import { resolveScope, sessionOf } from './registry.js'
|
|
|
5
5
|
import type { Registry } from './registry.js'
|
|
6
6
|
import { bucketOf } from './registry.js'
|
|
7
7
|
import { readText } from './pending/capture.js'
|
|
8
|
-
import { pendingRawId, trackPendingCover } from './pendingCompat.js'
|
|
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
|
-
//
|
|
89
|
-
|
|
85
|
+
// 与统一审查入口共用来源基线,不按查看者对话切分。
|
|
86
|
+
const page = loadPage(cwd, args.sessionId)
|
|
87
|
+
// 完整工作区列表重建唯一接管集合,清除所有来源的过期登记。
|
|
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,8 +123,8 @@ 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
|
-
//
|
|
128
|
-
if (removed && path) trackPendingCover(cwd, path, false)
|
|
126
|
+
// 移除后注销工作区唯一登记,其他对话不留过期副本。
|
|
127
|
+
if (removed && path) trackPendingCover(cwd, path, false, args.sessionId)
|
|
129
128
|
return { ok: true, removed }
|
|
130
129
|
},
|
|
131
130
|
}
|
|
@@ -168,7 +167,8 @@ function refreshHandlers(kit: ScopeKit): Partial<RpcHandlerMap> {
|
|
|
168
167
|
try {
|
|
169
168
|
const shell = shellOf(raw, policy)
|
|
170
169
|
const result = await importFromVcs(shell, cwd, args.sessionId, { includeUntracked: args.includeUntracked, path: args.path })
|
|
171
|
-
|
|
170
|
+
const page = loadPage(cwd, args.sessionId)
|
|
171
|
+
setPendingCovers(cwd, args.sessionId, page.items.map((item) => item.path))
|
|
172
172
|
return { ok: true, imported: result.imported, changes: result.changes }
|
|
173
173
|
} catch (error) {
|
|
174
174
|
return { ok: false, error: failureOf(error) }
|
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
|