dsh-vscode-mode 0.21.4 → 0.22.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 +50 -4
- package/lib/client.js +72 -98
- package/lib/client.js.map +2 -2
- package/lib/index.js +2419 -32
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/state/diffSummary.ts +43 -4
- package/src/client/ui/DiffLauncher.ts +1 -1
- package/src/client/ui/EditorView.ts +10 -2
- package/src/client/ui/OfficialDiffBar.tsx +1 -12
- package/src/client/ui/OfficialReview.ts +13 -3
- package/src/client/ui/SidecarDiffBar.tsx +0 -22
- package/src/index.ts +9 -0
- package/src/missingReview.ts +36 -14
- package/src/pending/capture.ts +136 -0
- package/src/pending/decide.ts +213 -0
- package/src/pending/hunks.ts +241 -0
- package/src/pending/kernel.ts +233 -0
- package/src/pending/persist.ts +324 -0
- package/src/pending/store.ts +467 -0
- package/src/pending/vcs.ts +444 -0
- package/src/pendingCompat.ts +189 -0
- package/src/pendingRpc.ts +128 -0
- package/src/reviewDb.ts +3 -0
- package/src/reviewRpc.ts +52 -1
- package/src/rpc.ts +29 -2
- package/src/shared/pendingReview.ts +105 -0
- package/src/shared/rpc.ts +18 -0
- package/src/store.ts +19 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** 待审投影 RPC:Keep/Revert 走累计基线,旧官方审查入口不再承接写操作。 @author ddj 2026年10月29号 */
|
|
2
|
+
import type { RpcHandlerMap } from './shared/rpc.js'
|
|
3
|
+
import type { PendingRequest } from './shared/pendingReview.js'
|
|
4
|
+
import { resolveScope, sessionOf } from './registry.js'
|
|
5
|
+
import type { Registry } from './registry.js'
|
|
6
|
+
import { bucketOf } from './registry.js'
|
|
7
|
+
import { readText } from './pending/capture.js'
|
|
8
|
+
import { pendingRawId, trackPendingCover } from './pendingCompat.js'
|
|
9
|
+
import { loadEntries, saveEntry } from './pending/persist.js'
|
|
10
|
+
import { applyDecide, importFromVcs, itemOf, loadDetail, loadPage, refreshed, removeEntry, undoDecide } from './pending/store.js'
|
|
11
|
+
import type { ShellLike } from './pending/vcs.js'
|
|
12
|
+
import type { Ctx } from './store.js'
|
|
13
|
+
import { policyOf } from './store.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @author ddj 2026年10月29号
|
|
17
|
+
* @description: 待审投影处理器。读路径允许跨实例冷读,写路径必须本端 live 会话。
|
|
18
|
+
* @param ctx Host 上下文
|
|
19
|
+
* @param registry 工作区记录桶缓存
|
|
20
|
+
* @returns 待审相关处理器表
|
|
21
|
+
*/
|
|
22
|
+
export function pendingHandlers(ctx: Ctx, registry: Registry): Partial<RpcHandlerMap> {
|
|
23
|
+
/** @private @author ddj 2026年10月29号 @param sessionId 请求作用域 @returns 工作区 */
|
|
24
|
+
async function workspace(sessionId: string): Promise<string> {
|
|
25
|
+
const scope = await resolveScope(ctx, sessionId)
|
|
26
|
+
if ('err' in scope) throw new Error('会话不存在或没有工作区')
|
|
27
|
+
await bucketOf(registry, ctx, scope.cwd)
|
|
28
|
+
return scope.cwd
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @private @author ddj 2026年10月29号 @param sessionId 请求作用域 @returns 本端 live 会话的工作区 */
|
|
32
|
+
async function liveWorkspace(sessionId: string): Promise<string> {
|
|
33
|
+
const cwd = await workspace(sessionId)
|
|
34
|
+
if (!sessionOf(ctx, sessionId)) throw new Error('写入要求本端活跃会话')
|
|
35
|
+
return cwd
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @private
|
|
40
|
+
* @author ddj 2026年10月30号
|
|
41
|
+
* @description: 面板列表里的条目 id 带待审前缀(用于和官方条目区分),
|
|
42
|
+
* 而待审内核按原始身份存取;各处理器统一走 pendingRawId 剥掉前缀。
|
|
43
|
+
* @param id 请求里的条目 id
|
|
44
|
+
* @returns 待审内核使用的原始 id
|
|
45
|
+
*/
|
|
46
|
+
const rawId = pendingRawId
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
'edrv.pending.list': async (args: PendingRequest) => {
|
|
50
|
+
const cwd = await workspace(args.sessionId)
|
|
51
|
+
const page = loadPage(cwd, args.sessionId)
|
|
52
|
+
// 列表即登记接管关系;旧写入口守卫之后只读内存集合,不再产生数据库读
|
|
53
|
+
for (const item of page.items) trackPendingCover(cwd, item.path, true)
|
|
54
|
+
return { ok: true, page }
|
|
55
|
+
},
|
|
56
|
+
'edrv.pending.detail': async (args: PendingRequest & { id: string }) => {
|
|
57
|
+
const cwd = await workspace(args.sessionId)
|
|
58
|
+
const detail = loadDetail(cwd, rawId(args.id))
|
|
59
|
+
return detail ? { ok: true, detail } : { ok: false, error: '待审条目不存在' }
|
|
60
|
+
},
|
|
61
|
+
'edrv.pending.decide': async (args) => {
|
|
62
|
+
const cwd = await liveWorkspace(args.sessionId)
|
|
63
|
+
try {
|
|
64
|
+
const id = rawId(args.id)
|
|
65
|
+
const result = await applyDecide(ctx.get('fs'), cwd, { ...args, id }, undefined, policyOf(ctx, sessionOf(ctx, args.sessionId)))
|
|
66
|
+
if (!result.ok) return { ok: false, error: result.reason }
|
|
67
|
+
const detail = loadDetail(cwd, id)
|
|
68
|
+
if (!detail) return { ok: false, error: '待审条目不存在' }
|
|
69
|
+
return { ok: true, item: detail.item, wrote: result.wrote }
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) }
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
'edrv.pending.remove': async (args) => {
|
|
75
|
+
const cwd = await liveWorkspace(args.sessionId)
|
|
76
|
+
const id = rawId(args.id)
|
|
77
|
+
const path = loadEntries(cwd).find((stored) => stored.entry.id === id)?.entry.path
|
|
78
|
+
const removed = removeEntry(cwd, id, args.revision)
|
|
79
|
+
// 移除后该路径不再由待审投影接管,旧入口守卫随之放行
|
|
80
|
+
if (removed && path) trackPendingCover(cwd, path, false)
|
|
81
|
+
return { ok: true, removed }
|
|
82
|
+
},
|
|
83
|
+
'edrv.pending.history': async (args) => {
|
|
84
|
+
const cwd = await liveWorkspace(args.sessionId)
|
|
85
|
+
try {
|
|
86
|
+
const result = await undoDecide(ctx.get('fs'), cwd, args, undefined, policyOf(ctx, sessionOf(ctx, args.sessionId)))
|
|
87
|
+
if (!result.ok) return { ok: false, error: result.reason }
|
|
88
|
+
return { ok: true, item: result.item, wrote: result.wrote }
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) }
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
'edrv.pending.import': async (args) => {
|
|
94
|
+
const cwd = await liveWorkspace(args.sessionId)
|
|
95
|
+
const raw = ctx.get('shell') as unknown as ShellLike | undefined
|
|
96
|
+
if (!raw) return { ok: false, error: '当前环境没有 shell 能力,无法读取版本库基线' }
|
|
97
|
+
// shell 执行器默认按 workspace-write 跑;本机没有可用沙箱后端时会直接拒绝,
|
|
98
|
+
// 所以按会话策略显式下发(会话已放宽时命令才能跑起来)。
|
|
99
|
+
const policy = policyOf(ctx, sessionOf(ctx, args.sessionId))
|
|
100
|
+
const shell: ShellLike = {
|
|
101
|
+
resolve: (request) => raw.resolve(policy ? { ...request, sandboxPolicy: policy } : request),
|
|
102
|
+
execute: (spec) => raw.execute(spec),
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const result = await importFromVcs(shell, cwd, args.sessionId, { includeUntracked: args.includeUntracked, path: args.path })
|
|
106
|
+
const page = loadPage(cwd, args.sessionId)
|
|
107
|
+
for (const item of page.items) trackPendingCover(cwd, item.path, true)
|
|
108
|
+
return { ok: true, imported: result.imported, changes: result.changes }
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) }
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
'edrv.pending.refresh': async (args: PendingRequest & { id: string }) => {
|
|
114
|
+
const cwd = await liveWorkspace(args.sessionId)
|
|
115
|
+
const id = rawId(args.id)
|
|
116
|
+
const stored = loadEntries(cwd).find((entry) => entry.entry.id === id)
|
|
117
|
+
if (!stored) return { ok: false, error: '待审条目不存在' }
|
|
118
|
+
const session = sessionOf(ctx, args.sessionId)
|
|
119
|
+
const fs = ctx.get('fs')
|
|
120
|
+
const target = await fs.resolve(id, { cwd, agent: session })
|
|
121
|
+
const content = await readText(fs, target).catch(() => undefined)
|
|
122
|
+
if (content === undefined) return { ok: false, error: '读取失败' }
|
|
123
|
+
const next = refreshed(stored, content, Date.now())
|
|
124
|
+
if (next) saveEntry(cwd, next)
|
|
125
|
+
return { ok: true, item: itemOf(next ?? stored) }
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/reviewDb.ts
CHANGED
|
@@ -207,6 +207,9 @@ export function reviewPage(cwd: string, cursor = 0, history = false, path?: stri
|
|
|
207
207
|
|
|
208
208
|
/** @public @author ddj 2026年10月26号 @param cwd Migrated workspace. @param path Legacy write target. @returns Whether any official version owns the file, including decided history. */
|
|
209
209
|
export function hasOfficial(cwd: string, path: string): boolean {
|
|
210
|
+
// 防御:解析结果若被误当成路径传进来(不透明对象),按「无官方记录」处理,
|
|
211
|
+
// 否则 SQLite 会把对象当命名参数绑定,抛出与真实原因无关的错误。
|
|
212
|
+
if (typeof path !== 'string') return false
|
|
210
213
|
const db = dbFor(cwd)
|
|
211
214
|
// A read guard must not acquire a migration write lock on legacy-only databases.
|
|
212
215
|
if (!db.prepare("SELECT 1 FROM sqlite_master WHERE name='review_files' AND type='table'").get()) return false
|
package/src/reviewRpc.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/** Session-scoped official review RPC. @author ddj 2026年10月26号 */
|
|
2
2
|
import type { RpcHandlerMap } from './shared/rpc.js'
|
|
3
|
+
import type { PendingPage } from './shared/pendingReview.js'
|
|
3
4
|
import { decideReview, reviewDetail, reviewPage } from './reviewDb.js'
|
|
4
5
|
import { undoReview } from './reviewUndo.js'
|
|
6
|
+
import { applyDecide, loadDetail, loadPage } from './pending/store.js'
|
|
7
|
+
import { PENDING_PREFIX, mergedPage, pendingDetailOf, pendingItemOf, pendingSamePath } from './pendingCompat.js'
|
|
5
8
|
import { bucketOf, resolveScope, sessionOf } from './registry.js'
|
|
6
9
|
import type { Registry } from './registry.js'
|
|
7
10
|
import type { Ctx } from './store.js'
|
|
@@ -31,19 +34,67 @@ export function reviewHandlers(ctx: Ctx, registry: Registry): Partial<RpcHandler
|
|
|
31
34
|
await bucketOf(registry, ctx, scope.cwd)
|
|
32
35
|
return scope.cwd
|
|
33
36
|
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @private
|
|
40
|
+
* @author ddj 2026年10月30号
|
|
41
|
+
* @description: 读取待审页;待审库尚未建立或迁移失败时按空页处理。
|
|
42
|
+
* 待审投影是增量能力,不能因为它让官方列表整体不可用。
|
|
43
|
+
* @param cwd 工作区
|
|
44
|
+
* @param sessionId 请求会话
|
|
45
|
+
* @returns 待审页(失败为空页)
|
|
46
|
+
*/
|
|
47
|
+
function pendingPageOf(cwd: string, sessionId: string): PendingPage {
|
|
48
|
+
try { return loadPage(cwd, sessionId) }
|
|
49
|
+
catch { return { items: [], pending: 0, nextCursor: null, gaps: [] } }
|
|
50
|
+
}
|
|
51
|
+
|
|
34
52
|
return {
|
|
35
53
|
'edrv.review.list': async (args) => {
|
|
36
54
|
const cwd = await workspace(args.sessionId)
|
|
37
|
-
|
|
55
|
+
const official = reviewPage(cwd, args.cursor, args.history, args.path)
|
|
56
|
+
const pending = pendingPageOf(cwd, args.sessionId)
|
|
57
|
+
// 历史快照只属于官方通道;待审条目没有轮次历史,混进去会造出假的「轮次 0」。
|
|
58
|
+
if (args.history) return { ok: true, page: official }
|
|
59
|
+
if (args.path !== undefined) {
|
|
60
|
+
// 单文件台账(差异栏「详情」展开处)必须并上该路径的待审条目,
|
|
61
|
+
// 否则待审详情(块判定 + 批注 + 让 AI 应答)在这条入口上永远点不到。
|
|
62
|
+
const key = args.path
|
|
63
|
+
const own = pending.items.filter((item) => pendingSamePath(item.path, key)).map((item) => pendingItemOf(item, cwd))
|
|
64
|
+
// 待审接管后同路径的官方轮次行只是旧口径残影:留着会让人点进只读详情,以为没有判定入口。
|
|
65
|
+
const history = own.length ? official.items.filter((item) => !pendingSamePath(item.path, key)) : official.items
|
|
66
|
+
return { ok: true, page: { ...official, items: [...own, ...history] } }
|
|
67
|
+
}
|
|
68
|
+
// 待审投影就位后由它承接被覆盖路径,官方旧行让位,同一路径只保留一个权威
|
|
69
|
+
return { ok: true, page: mergedPage(official, pending, cwd) }
|
|
38
70
|
},
|
|
39
71
|
'edrv.review.detail': async (args) => {
|
|
40
72
|
const cwd = await workspace(args.sessionId)
|
|
73
|
+
if (args.id.startsWith(PENDING_PREFIX)) {
|
|
74
|
+
const detail = loadDetail(cwd, args.id.slice(PENDING_PREFIX.length))
|
|
75
|
+
if (!detail) return { ok: false, error: '待审条目不存在' }
|
|
76
|
+
return { ok: true, detail: pendingDetailOf(detail, new Date(detail.item.updatedAt).toISOString(), cwd) }
|
|
77
|
+
}
|
|
41
78
|
const detail = reviewDetail(cwd, args.id)
|
|
42
79
|
return detail ? { ok: true, detail } : { ok: false, error: '官方审查记录不存在' }
|
|
43
80
|
},
|
|
44
81
|
'edrv.review.decide': async (args) => {
|
|
45
82
|
// 写类操作要求 live 会话:避免两端并发写同一工作区审查库,回滚也依赖本端会话内容
|
|
46
83
|
const cwd = await liveWorkspace(args.sessionId)
|
|
84
|
+
if (args.id.startsWith(PENDING_PREFIX)) {
|
|
85
|
+
// 待审条目走累计基线:Keep 零写盘,Revert 写回并读回校验
|
|
86
|
+
const result = await applyDecide(ctx.get('fs'), cwd, {
|
|
87
|
+
sessionId: args.sessionId,
|
|
88
|
+
id: args.id.slice(PENDING_PREFIX.length),
|
|
89
|
+
revision: args.revision,
|
|
90
|
+
operationId: args.operationId,
|
|
91
|
+
decision: args.decision,
|
|
92
|
+
scope: args.scope === 'hunk' ? 'block' : 'file',
|
|
93
|
+
...(args.blockId === undefined ? {} : { blockId: args.blockId }),
|
|
94
|
+
})
|
|
95
|
+
if (!result.ok) return { ok: false, error: result.reason }
|
|
96
|
+
return { ok: true, revision: result.flags.revision }
|
|
97
|
+
}
|
|
47
98
|
try {
|
|
48
99
|
const result = args.decision === 'rejected'
|
|
49
100
|
? await undoReview(ctx, sessionOf(ctx, args.sessionId), cwd, args)
|
package/src/rpc.ts
CHANGED
|
@@ -60,7 +60,9 @@ import type { FileVersions } from './fileVersions.js'
|
|
|
60
60
|
import { settingsDocPath } from './settingsDocPath.js'
|
|
61
61
|
import { createTextPages } from './textPages.js'
|
|
62
62
|
import { reviewHandlers } from './reviewRpc.js'
|
|
63
|
-
import {
|
|
63
|
+
import { pendingHandlers } from './pendingRpc.js'
|
|
64
|
+
import { fileAbsent, missingReview, missingDecide, missingPath } from './missingReview.js'
|
|
65
|
+
import { pendingCovers } from './pendingCompat.js'
|
|
64
66
|
|
|
65
67
|
/** Process-local cursor signer; reads must stay on the Host filesystem capability. */
|
|
66
68
|
const readTextPage = createTextPages()
|
|
@@ -103,6 +105,19 @@ function recView(record: DiffRecord, ws: RemoteWs | null = null): RecordView {
|
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
/**
|
|
109
|
+
* 旧写入口守卫:路径已由待审累计基线接管时一律拒绝,避免两套权威同时写同一文件。
|
|
110
|
+
* @author ddj 2026年10月29号
|
|
111
|
+
* @param cwd 工作区
|
|
112
|
+
* @param path 请求涉及的路径(可为空表示无法定位)
|
|
113
|
+
* @returns 拒绝原因,未接管时为 null
|
|
114
|
+
*/
|
|
115
|
+
function legacyBlocked(cwd: string, path: string | undefined): string | null {
|
|
116
|
+
if (!path) return null
|
|
117
|
+
if (!pendingCovers(cwd, path)) return null
|
|
118
|
+
return '该文件已由待审累计基线接管,请使用面板 Keep/Revert(edrv.review.decide)'
|
|
119
|
+
}
|
|
120
|
+
|
|
106
121
|
/** 会话/工作区公共前置:返回 {session,cwd} 或错误文案。 */
|
|
107
122
|
async function requireSession(ctx: Ctx, sessionId: string | undefined): Promise<
|
|
108
123
|
{ session: Session; cwd: string } | { err: string }
|
|
@@ -600,6 +615,7 @@ export function buildHandlers(
|
|
|
600
615
|
// edrv.dap.* 由 createDapRpc 提供(调试会话单例),这里并入。
|
|
601
616
|
...((dapHandlers ?? {}) as RpcHandlerMap),
|
|
602
617
|
...(reviewHandlers(ctx, registry) as RpcHandlerMap),
|
|
618
|
+
...(pendingHandlers(ctx, registry) as RpcHandlerMap),
|
|
603
619
|
'edrv.list': async (args) => {
|
|
604
620
|
// #region debug log
|
|
605
621
|
const start = performance.now()
|
|
@@ -688,6 +704,8 @@ export function buildHandlers(
|
|
|
688
704
|
const sc = await requireSession(ctx, args.sessionId)
|
|
689
705
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
690
706
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
707
|
+
const blocked = legacyBlocked(sc.cwd, bucket.get(args.callId)?.path)
|
|
708
|
+
if (blocked) return { ok: false, error: blocked }
|
|
691
709
|
const results = await applyDecisions(ctx, sc.session, sc.cwd, bucket, [{ callId: args.callId, scope: args.scope, hunkIndex: args.hunkIndex, decision: 'accepted' }])
|
|
692
710
|
const item = results[0]
|
|
693
711
|
if (!item?.ok) return { ok: false, error: item?.error ?? '操作失败' }
|
|
@@ -697,6 +715,8 @@ export function buildHandlers(
|
|
|
697
715
|
const sc = await requireSession(ctx, args.sessionId)
|
|
698
716
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
699
717
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
718
|
+
const blocked = legacyBlocked(sc.cwd, bucket.get(args.callId)?.path)
|
|
719
|
+
if (blocked) return { ok: false, error: blocked }
|
|
700
720
|
const results = await applyDecisions(ctx, sc.session, sc.cwd, bucket, [{ callId: args.callId, scope: args.scope, hunkIndex: args.hunkIndex, decision: 'rejected' }])
|
|
701
721
|
const item = results[0]
|
|
702
722
|
if (!item?.ok) return { ok: false, error: item?.error ?? '操作失败' }
|
|
@@ -705,6 +725,8 @@ export function buildHandlers(
|
|
|
705
725
|
'edrv.missingReview': async (args) => {
|
|
706
726
|
const sc = await requireSession(ctx, args.sessionId)
|
|
707
727
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
728
|
+
const blocked = legacyBlocked(sc.cwd, args.path)
|
|
729
|
+
if (blocked) return { ok: false, error: blocked }
|
|
708
730
|
try {
|
|
709
731
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
710
732
|
if (!await fileAbsent(ctx, sc.cwd, args.path)) return { ok: true, review: null }
|
|
@@ -714,6 +736,8 @@ export function buildHandlers(
|
|
|
714
736
|
'edrv.missingDecide': async (args) => {
|
|
715
737
|
const sc = await requireSession(ctx, args.sessionId)
|
|
716
738
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
739
|
+
const blocked = legacyBlocked(sc.cwd, missingPath(args.token))
|
|
740
|
+
if (blocked) return { ok: false, error: blocked }
|
|
717
741
|
try {
|
|
718
742
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
719
743
|
return { ok: true, callIds: await missingDecide(ctx, sc.session, sc.cwd, bucket, args.token, args.operationId, args.undo === true) }
|
|
@@ -723,7 +747,10 @@ export function buildHandlers(
|
|
|
723
747
|
const sc = await requireSession(ctx, args.sessionId)
|
|
724
748
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
725
749
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
726
|
-
const
|
|
750
|
+
const items = Array.isArray(args.items) ? args.items : []
|
|
751
|
+
const blocked = items.map((item) => legacyBlocked(sc.cwd, bucket.get(item.callId)?.path)).find((reason) => reason !== null)
|
|
752
|
+
if (blocked) return { ok: false, error: blocked }
|
|
753
|
+
const results = await applyDecisions(ctx, sc.session, sc.cwd, bucket, items)
|
|
727
754
|
return { ok: true, results }
|
|
728
755
|
},
|
|
729
756
|
/** @author ddj 2026年10月22号 @param args Read-only request. @returns Bounded official range read, or unsupported on older providers; never bypasses Host IO. */
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode 待审投影(pending review)的共享契约:按文件累计的净差异视图。
|
|
3
|
+
* 官方快照表是不可变事实来源,本类型只是可由事实重建的投影,任何决定都不等于磁盘版本。
|
|
4
|
+
* @author ddj 2026年10月29号
|
|
5
|
+
*/
|
|
6
|
+
import type { OfficialDiff } from './review.js'
|
|
7
|
+
import type { ReviewBlockState } from './reviewRegions.js'
|
|
8
|
+
|
|
9
|
+
/** 基线形态:`file` 写回内容,`none` 说明基线没有该路径(Revert 语义是删除文件)。 */
|
|
10
|
+
export type PendingEarlier = 'file' | 'none'
|
|
11
|
+
|
|
12
|
+
/** 会话血缘,用于子代理/派生会话归属到同一待审视图。 */
|
|
13
|
+
export interface PendingLineage {
|
|
14
|
+
parentSessionId?: string
|
|
15
|
+
origin?: string
|
|
16
|
+
delegationDepth?: number
|
|
17
|
+
cwd?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 待审条目的展示投影。 */
|
|
21
|
+
export interface PendingItem {
|
|
22
|
+
/** 稳定身份键(后端 + 目标键),客户端按它发送动作。 */
|
|
23
|
+
id: string
|
|
24
|
+
/** 展示路径。 */
|
|
25
|
+
path: string
|
|
26
|
+
earlierVersion: PendingEarlier
|
|
27
|
+
added: number
|
|
28
|
+
deleted: number
|
|
29
|
+
/** oldText 与 newText 严格相等:已解决但仍留在列表。 */
|
|
30
|
+
settled: boolean
|
|
31
|
+
/** 读者还没看过的最新变化。 */
|
|
32
|
+
unseen: boolean
|
|
33
|
+
/** 文件已不存在(Revert 可恢复)。 */
|
|
34
|
+
missing: boolean
|
|
35
|
+
/** 当前文件内容已被外部改动替换过。 */
|
|
36
|
+
diverged: boolean
|
|
37
|
+
/** 文本超出存储/展示上限,差异与决定能力受限。 */
|
|
38
|
+
oversized: boolean
|
|
39
|
+
revision: number
|
|
40
|
+
updatedAt: number
|
|
41
|
+
sessionId: string
|
|
42
|
+
sessionIds: string[]
|
|
43
|
+
lineage?: PendingLineage
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 一页待审条目。 */
|
|
47
|
+
export interface PendingPage {
|
|
48
|
+
items: PendingItem[]
|
|
49
|
+
pending: number
|
|
50
|
+
nextCursor: number | null
|
|
51
|
+
/** 采集/重建过程中的缺口说明,绝不静默吞掉。 */
|
|
52
|
+
gaps: string[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 单条待审详情。 */
|
|
56
|
+
export interface PendingDetail {
|
|
57
|
+
item: PendingItem
|
|
58
|
+
diff: OfficialDiff
|
|
59
|
+
blocks: ReviewBlockState[]
|
|
60
|
+
capability: { keep: boolean; revert: boolean; reason: string }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 全部请求都带会话作用域,工作区由 host 解析,永不采信客户端 cwd。 */
|
|
64
|
+
export interface PendingRequest {
|
|
65
|
+
sessionId: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 决定粒度。 */
|
|
69
|
+
export interface PendingScope {
|
|
70
|
+
scope: 'file' | 'block' | 'range' | 'all'
|
|
71
|
+
blockId?: string
|
|
72
|
+
startLine?: number
|
|
73
|
+
endLine?: number
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Keep/Revert 请求;operationId 提供幂等,revision 提供并发校验。 */
|
|
77
|
+
export interface PendingDecide extends PendingRequest, PendingScope {
|
|
78
|
+
id: string
|
|
79
|
+
revision: number
|
|
80
|
+
operationId: string
|
|
81
|
+
decision: 'accepted' | 'rejected'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 从列表移除已解决条目(不触碰磁盘)。 */
|
|
85
|
+
export interface PendingRemove extends PendingRequest {
|
|
86
|
+
id: string
|
|
87
|
+
revision: number
|
|
88
|
+
operationId: string
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 撤销/重做一次决定。 */
|
|
92
|
+
export interface PendingHistoryMove extends PendingRequest {
|
|
93
|
+
direction: 'undo' | 'redo'
|
|
94
|
+
operationId: string
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 从版本控制导入本地改动。 */
|
|
98
|
+
export interface PendingImport extends PendingRequest {
|
|
99
|
+
includeUntracked?: boolean
|
|
100
|
+
/** 只刷新一个文件,便于大仓单文件重扫。 */
|
|
101
|
+
path?: string
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 待审条目在面板列表里的 id 前缀;客户端与 Host 共用这一份口径。 */
|
|
105
|
+
export const PENDING_PREFIX = 'pending:'
|
package/src/shared/rpc.ts
CHANGED
|
@@ -26,6 +26,10 @@ import { appUrl } from './appUrl.js'
|
|
|
26
26
|
import type { LoggerLevel } from './logger.js'
|
|
27
27
|
import type { SvnViewRequest, SvnViewPayload } from './svnExplorer.js'
|
|
28
28
|
import type { DecideReview, ReviewDetail, ReviewPage, ReviewRequest } from './review.js'
|
|
29
|
+
import type {
|
|
30
|
+
PendingDecide, PendingDetail, PendingHistoryMove, PendingImport,
|
|
31
|
+
PendingItem, PendingPage, PendingRemove, PendingRequest,
|
|
32
|
+
} from './pendingReview.js'
|
|
29
33
|
|
|
30
34
|
/** webServer 精确路由。 */
|
|
31
35
|
export const RPC_PATH = '/edrv/rpc'
|
|
@@ -372,6 +376,13 @@ export interface RpcRequestMap {
|
|
|
372
376
|
'edrv.review.list': ReviewRequest & { cursor?: number; history?: boolean; path?: string }
|
|
373
377
|
'edrv.review.detail': ReviewRequest & { id: string }
|
|
374
378
|
'edrv.review.decide': DecideReview
|
|
379
|
+
'edrv.pending.list': PendingRequest
|
|
380
|
+
'edrv.pending.detail': PendingRequest & { id: string }
|
|
381
|
+
'edrv.pending.decide': PendingDecide
|
|
382
|
+
'edrv.pending.remove': PendingRemove
|
|
383
|
+
'edrv.pending.refresh': PendingRequest & { id: string }
|
|
384
|
+
'edrv.pending.history': PendingHistoryMove
|
|
385
|
+
'edrv.pending.import': PendingImport
|
|
375
386
|
'edrv.list': { sessionId?: string; callIds?: string[]; skipStale?: boolean }
|
|
376
387
|
'edrv.accept': { sessionId?: string; callId: string; scope?: RpcScope; hunkIndex?: number }
|
|
377
388
|
'edrv.reject': { sessionId?: string; callId: string; scope?: RpcScope; hunkIndex?: number }
|
|
@@ -546,6 +557,13 @@ export interface RpcOkMap {
|
|
|
546
557
|
'edrv.review.list': { page: ReviewPage }
|
|
547
558
|
'edrv.review.detail': { detail: ReviewDetail }
|
|
548
559
|
'edrv.review.decide': { revision: number }
|
|
560
|
+
'edrv.pending.list': { page: PendingPage }
|
|
561
|
+
'edrv.pending.detail': { detail: PendingDetail }
|
|
562
|
+
'edrv.pending.decide': { item: PendingItem; wrote: boolean }
|
|
563
|
+
'edrv.pending.remove': { removed: boolean }
|
|
564
|
+
'edrv.pending.refresh': { item: PendingItem }
|
|
565
|
+
'edrv.pending.history': { item: PendingItem; wrote: boolean }
|
|
566
|
+
'edrv.pending.import': { imported: number; changes: number }
|
|
549
567
|
'edrv.list': { records: RecordView[] }
|
|
550
568
|
'edrv.accept': { record: RecordView }
|
|
551
569
|
'edrv.reject': { record: RecordView }
|
package/src/store.ts
CHANGED
|
@@ -78,6 +78,25 @@ export async function resolveTargetIn(ctx: Ctx, cwd: string, path: string): Prom
|
|
|
78
78
|
return fs.resolve(path, cwd ? { cwd } : {})
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* 把解析结果还原成宿主路径字符串。
|
|
83
|
+
* 官方 provider 的 resolve 返回不透明 FsTarget,而多数调用点按字符串使用;
|
|
84
|
+
* 直接把对象当路径传下去会落进 SQL 绑定或 path.resolve,报出与真实原因无关的错误。
|
|
85
|
+
* @author ddj 2026年10月30号
|
|
86
|
+
* @param ctx DSH 上下文
|
|
87
|
+
* @param target resolve 的返回值:宿主路径字符串或 FsTarget
|
|
88
|
+
* @returns 宿主路径字符串;无法还原时返回空串
|
|
89
|
+
*/
|
|
90
|
+
export function targetPathOf(ctx: Ctx, target: unknown): string {
|
|
91
|
+
if (typeof target === 'string') return target
|
|
92
|
+
const fs = ctx.get('fs')
|
|
93
|
+
if (typeof fs?.processPath !== 'function') return ''
|
|
94
|
+
try {
|
|
95
|
+
const resolved = fs.processPath(target as never)
|
|
96
|
+
return typeof resolved === 'string' && resolved !== '[object Object]' ? resolved : ''
|
|
97
|
+
} catch { return '' }
|
|
98
|
+
}
|
|
99
|
+
|
|
81
100
|
/** 读取工作区主 sidecar 文本(缺失/失败 → null)。 */
|
|
82
101
|
async function readSidecarText(ctx: Ctx, cwd: string | null): Promise<string | null> {
|
|
83
102
|
const fs = ctx.get('fs')
|