dsh-vscode-mode 0.23.1 → 0.23.3
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 +3 -0
- package/lib/client.js +234 -47
- package/lib/client.js.map +2 -2
- package/lib/index.js +221 -46
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/missingReview.ts +24 -0
- package/src/client/reviewBatch.ts +5 -0
- package/src/client/ui/EditorView.ts +172 -19
- package/src/client/ui/OfficialDiffBar.tsx +18 -3
- package/src/client/ui/SidecarDiffBar.tsx +3 -2
- package/src/client/ui/reviewAllRow.tsx +5 -4
- package/src/missingReview.ts +44 -5
- package/src/officialCapture.ts +11 -0
- package/src/reviewDb.ts +23 -0
- package/src/reviewRpc.ts +140 -37
- package/src/rpc.ts +6 -2
- package/src/shared/review.ts +2 -1
- package/src/shared/rpc.ts +4 -1
package/src/reviewRpc.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Session-scoped official review RPC. @author ddj 2026年10月26号 */
|
|
2
2
|
import type { RpcHandlerMap } from './shared/rpc.js'
|
|
3
|
-
import type { ReviewItem, ReviewPage } from './shared/review.js'
|
|
3
|
+
import type { ReviewDetail, ReviewItem, ReviewPage } from './shared/review.js'
|
|
4
4
|
import type { PendingPage } from './shared/pendingReview.js'
|
|
5
5
|
import { decideReview, reviewDetail, reviewPage } from './reviewDb.js'
|
|
6
6
|
import { undoReview } from './reviewUndo.js'
|
|
@@ -8,11 +8,21 @@ import { applyDecide, loadDetail, loadPage } from './pending/store.js'
|
|
|
8
8
|
import { PENDING_PREFIX, mergedPage, pendingDetailOf, pendingItemOf, pendingSamePath } from './pendingCompat.js'
|
|
9
9
|
import { bucketOf, resolveScope, sessionOf } from './registry.js'
|
|
10
10
|
import type { Registry } from './registry.js'
|
|
11
|
+
import { resolveTargetIn } from './store.js'
|
|
11
12
|
import type { Ctx } from './store.js'
|
|
13
|
+
import { log } from './log.js'
|
|
12
14
|
|
|
13
15
|
/** 批量写入快照的硬上限:待审积压超过它说明状态异常,宁可拒绝也不做无界写循环。 */
|
|
14
16
|
const BATCH_LIMIT = 2000
|
|
15
17
|
|
|
18
|
+
/** 冷端 Undo 说明:Undo 会校验证据并写回源文件,只能在持有该会话的那一端执行。 */
|
|
19
|
+
const UNDO_NEEDS_LIVE = 'Undo 需要在本端活跃的会话上执行:该会话当前在另一端(Web / 桌面)激活。'
|
|
20
|
+
+ 'Keep 不受影响,可在任一端执行;或在本端打开该会话后重试。'
|
|
21
|
+
|
|
22
|
+
/** 冷端待审判定说明:待审库是按工作区的本地存储,判定需要与正在编辑的那一端保持一致。 */
|
|
23
|
+
const PENDING_NEEDS_LIVE = '待审条目的判定需要在本端活跃的会话上执行:该会话当前在另一端激活,'
|
|
24
|
+
+ '请在那一端处理,或在本端打开该会话后重试。'
|
|
25
|
+
|
|
16
26
|
/**
|
|
17
27
|
* @private
|
|
18
28
|
* @author ddj 2026年10月31号
|
|
@@ -45,6 +55,35 @@ function batchSnapshot(cwd: string, pending: PendingPage): { ok: true; page: Rev
|
|
|
45
55
|
}
|
|
46
56
|
}
|
|
47
57
|
|
|
58
|
+
/**
|
|
59
|
+
* @private
|
|
60
|
+
* @author ddj 2026年10月31号
|
|
61
|
+
* @description: 文件已在磁盘删除时修正官方记录的能力说明。Keep 只写审查库、仍可结案,
|
|
62
|
+
* Undo 需要读写源文件、必然不可用:把「缺少原始字节」这种误导性原因换成可执行说明。
|
|
63
|
+
* 只改文案与 undo 位,不参与判定写入。
|
|
64
|
+
* @param ctx Host 能力
|
|
65
|
+
* @param cwd 工作区
|
|
66
|
+
* @param detail 官方记录详情
|
|
67
|
+
* @param capability 已按冷/热端调整过的能力
|
|
68
|
+
* @returns 修正后的能力;无法确认删除时原样返回
|
|
69
|
+
*/
|
|
70
|
+
async function absenceCapability(ctx: Ctx, cwd: string, detail: ReviewDetail, capability: ReviewDetail['capability']): Promise<ReviewDetail['capability']> {
|
|
71
|
+
if (!capability.keep) return capability
|
|
72
|
+
// 记录本身就是删除(after=false):Undo 要整文件内容,官方差异只有片段
|
|
73
|
+
if (detail.diff.kind === 'text' && !detail.diff.after) {
|
|
74
|
+
return { ...capability, undo: false, reason: '该记录删除了这个文件:Keep 按删除结案;Undo 需要完整文件内容,官方差异只有片段,不可用。' }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 文件已被删除时,Undo 的失败原因是「文件不在」,不是「缺少原始字节」:先查磁盘再谈证据
|
|
78
|
+
const fs = ctx.get('fs')
|
|
79
|
+
if (!fs) return capability
|
|
80
|
+
try {
|
|
81
|
+
const target = await resolveTargetIn(ctx, cwd, detail.item.currentPath)
|
|
82
|
+
if (await fs.stat(target)) return capability
|
|
83
|
+
} catch { return capability }
|
|
84
|
+
return { ...capability, undo: false, reason: '文件已在磁盘删除:Keep 按删除结案;Undo 需要读写该文件,当前不可用。' }
|
|
85
|
+
}
|
|
86
|
+
|
|
48
87
|
/**
|
|
49
88
|
* @public
|
|
50
89
|
* @author ddj 2026年10月26号 / 2026年10月28号
|
|
@@ -55,20 +94,52 @@ function batchSnapshot(cwd: string, pending: PendingPage): { ok: true; page: Rev
|
|
|
55
94
|
* @returns 审查相关处理器表
|
|
56
95
|
*/
|
|
57
96
|
export function reviewHandlers(ctx: Ctx, registry: Registry): Partial<RpcHandlerMap> {
|
|
58
|
-
/**
|
|
59
|
-
|
|
97
|
+
/**
|
|
98
|
+
* @private
|
|
99
|
+
* @author ddj 2026年10月31号
|
|
100
|
+
* @description: 解析会话作用域,并标明本端是否持有该会话的活跃实例。
|
|
101
|
+
* 工作区一律由 host 解析,绝不采信调用方 cwd;冷读作用域只带 cwd,不带会话实例。
|
|
102
|
+
* @param sessionId 请求作用域
|
|
103
|
+
* @returns 已迁移工作区与活跃标记
|
|
104
|
+
* @throws 会话不存在、会话无工作区或活跃库迁移未完成
|
|
105
|
+
*/
|
|
106
|
+
async function scopeOf(sessionId: string): Promise<{ cwd: string; live: boolean }> {
|
|
60
107
|
const scope = await resolveScope(ctx, sessionId)
|
|
61
108
|
if ('err' in scope) throw new Error('会话不存在或没有工作区')
|
|
109
|
+
|
|
62
110
|
await bucketOf(registry, ctx, scope.cwd)
|
|
63
|
-
return scope.cwd
|
|
111
|
+
return { cwd: scope.cwd, live: !!scope.session }
|
|
64
112
|
}
|
|
65
113
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
114
|
+
/**
|
|
115
|
+
* @private
|
|
116
|
+
* @author ddj 2026年10月31号
|
|
117
|
+
* @description: 本端是否持有该会话的活跃实例(纯诊断用,不解析工作区)。
|
|
118
|
+
* @param sessionId 请求作用域
|
|
119
|
+
* @returns 本端加载了该会话时为 true
|
|
120
|
+
*/
|
|
121
|
+
function liveOf(sessionId: string): boolean {
|
|
122
|
+
return !!sessionOf(ctx, sessionId)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @private
|
|
127
|
+
* @author ddj 2026年10月31号
|
|
128
|
+
* @description: 按本端是否活跃覆写面板能力位。活跃端保持数据库结论;
|
|
129
|
+
* 冷端关闭依赖会话的操作(Undo、待审判定)并给出跨端原因,Keep 仍按数据库结论放行。
|
|
130
|
+
* @param capability 数据库推导的能力位
|
|
131
|
+
* @param live 本端是否持有活跃会话
|
|
132
|
+
* @param pending 是否为待审投影条目
|
|
133
|
+
* @returns 面板可用的能力位
|
|
134
|
+
*/
|
|
135
|
+
function liveCapabilityOf(
|
|
136
|
+
capability: { keep: boolean; undo: boolean; reason: string },
|
|
137
|
+
live: boolean,
|
|
138
|
+
pending: boolean,
|
|
139
|
+
): { keep: boolean; undo: boolean; reason: string; live: boolean } {
|
|
140
|
+
if (live) return { ...capability, live: true }
|
|
141
|
+
if (pending) return { keep: false, undo: false, reason: PENDING_NEEDS_LIVE, live: false }
|
|
142
|
+
return { ...capability, undo: false, live: false, reason: capability.undo ? UNDO_NEEDS_LIVE : capability.reason }
|
|
72
143
|
}
|
|
73
144
|
|
|
74
145
|
/**
|
|
@@ -81,13 +152,19 @@ export function reviewHandlers(ctx: Ctx, registry: Registry): Partial<RpcHandler
|
|
|
81
152
|
* @returns 待审页(失败为空页)
|
|
82
153
|
*/
|
|
83
154
|
function pendingPageOf(cwd: string, sessionId: string): PendingPage {
|
|
84
|
-
try {
|
|
85
|
-
|
|
155
|
+
try {
|
|
156
|
+
const page = loadPage(cwd, sessionId)
|
|
157
|
+
// 已结算(老文本与新文本一致)的投影没有净变化可审:留着会顶掉同路径的官方待审记录,
|
|
158
|
+
// 让差异栏选中一条 Keep 不可用的条目,表现为「按钮亮着却点不动」。
|
|
159
|
+
return page.items.some((item) => item.settled === true)
|
|
160
|
+
? { ...page, items: page.items.filter((item) => item.settled !== true) }
|
|
161
|
+
: page
|
|
162
|
+
} catch { return { items: [], pending: 0, nextCursor: null, gaps: [] } }
|
|
86
163
|
}
|
|
87
164
|
|
|
88
165
|
return {
|
|
89
166
|
'edrv.review.list': async (args) => {
|
|
90
|
-
const cwd = await
|
|
167
|
+
const { cwd } = await scopeOf(args.sessionId)
|
|
91
168
|
// 批量通道要的是一次性完整快照:分页口径的 pending 只是页内展示计数,不能当完整性不变式
|
|
92
169
|
if (args.all && !args.history && args.path === undefined) {
|
|
93
170
|
return batchSnapshot(cwd, pendingPageOf(cwd, args.sessionId))
|
|
@@ -106,43 +183,69 @@ export function reviewHandlers(ctx: Ctx, registry: Registry): Partial<RpcHandler
|
|
|
106
183
|
return { ok: true, page: { ...official, items: [...own, ...history] } }
|
|
107
184
|
}
|
|
108
185
|
// 待审投影就位后由它承接被覆盖路径,官方旧行让位,同一路径只保留一个权威
|
|
109
|
-
|
|
186
|
+
const merged = mergedPage(official, pending, cwd)
|
|
187
|
+
log.debug(`[review] list live=${liveOf(args.sessionId) ? 1 : 0} path=${args.path ?? '(all)'}`
|
|
188
|
+
+ ` items=${merged.items.length} pending=${merged.pending} covered=${merged.overview?.coveredPaths.length ?? -1}`)
|
|
189
|
+
return { ok: true, page: merged }
|
|
110
190
|
},
|
|
111
191
|
'edrv.review.detail': async (args) => {
|
|
112
|
-
const
|
|
192
|
+
const scope = await scopeOf(args.sessionId)
|
|
193
|
+
const cwd = scope.cwd
|
|
113
194
|
if (args.id.startsWith(PENDING_PREFIX)) {
|
|
114
195
|
const detail = loadDetail(cwd, args.id.slice(PENDING_PREFIX.length))
|
|
115
196
|
if (!detail) return { ok: false, error: '待审条目不存在' }
|
|
116
|
-
|
|
197
|
+
|
|
198
|
+
const mapped = pendingDetailOf(detail, new Date(detail.item.updatedAt).toISOString(), cwd)
|
|
199
|
+
// 待审判定走本地待审库,跨端会与另一端 agent 的写入抢同一份 JSON,故冷端一律关闭
|
|
200
|
+
return { ok: true, detail: { ...mapped, capability: liveCapabilityOf(mapped.capability, scope.live, true) } }
|
|
117
201
|
}
|
|
202
|
+
|
|
118
203
|
const detail = reviewDetail(cwd, args.id)
|
|
119
|
-
|
|
204
|
+
if (!detail) {
|
|
205
|
+
log.debug(`[review] detail live=${scope.live ? 1 : 0} id=${args.id} found=0`)
|
|
206
|
+
return { ok: false, error: '官方审查记录不存在' }
|
|
207
|
+
}
|
|
208
|
+
// Keep 只写共享库,冷端放行;Undo 依赖会话证据与源文件写回,冷端关闭
|
|
209
|
+
const capability = await absenceCapability(ctx, cwd, detail, liveCapabilityOf(detail.capability, scope.live, false))
|
|
210
|
+
log.debug(`[review] detail live=${scope.live ? 1 : 0} id=${args.id} found=1`
|
|
211
|
+
+ ` keep=${capability.keep ? 1 : 0} undo=${capability.undo ? 1 : 0}`)
|
|
212
|
+
return { ok: true, detail: { ...detail, capability } }
|
|
120
213
|
},
|
|
121
214
|
'edrv.review.decide': async (args) => {
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
if (args.id.startsWith(PENDING_PREFIX)) {
|
|
125
|
-
// 待审条目走累计基线:Keep 零写盘,Revert 写回并读回校验
|
|
126
|
-
const result = await applyDecide(ctx.get('fs'), cwd, {
|
|
127
|
-
sessionId: args.sessionId,
|
|
128
|
-
id: args.id.slice(PENDING_PREFIX.length),
|
|
129
|
-
revision: args.revision,
|
|
130
|
-
operationId: args.operationId,
|
|
131
|
-
decision: args.decision,
|
|
132
|
-
scope: args.scope === 'hunk' ? 'block' : 'file',
|
|
133
|
-
...(args.blockId === undefined ? {} : { blockId: args.blockId }),
|
|
134
|
-
})
|
|
135
|
-
if (!result.ok) return { ok: false, error: result.reason }
|
|
136
|
-
return { ok: true, revision: result.flags.revision }
|
|
137
|
-
}
|
|
215
|
+
// Keep 只写共享审查库(CAS + operationId 幂等 + WAL),冷端也放行;
|
|
216
|
+
// Undo 与待审判定依赖本端会话内容或本地待审库,冷端返回可读原因而不是 500。
|
|
138
217
|
try {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
:
|
|
218
|
+
const scope = await scopeOf(args.sessionId)
|
|
219
|
+
if (args.id.startsWith(PENDING_PREFIX)) {
|
|
220
|
+
if (!scope.live) return { ok: false, error: PENDING_NEEDS_LIVE }
|
|
221
|
+
// 待审条目走累计基线:Keep 零写盘,Revert 写回并读回校验
|
|
222
|
+
const result = await applyDecide(ctx.get('fs'), scope.cwd, {
|
|
223
|
+
sessionId: args.sessionId,
|
|
224
|
+
id: args.id.slice(PENDING_PREFIX.length),
|
|
225
|
+
revision: args.revision,
|
|
226
|
+
operationId: args.operationId,
|
|
227
|
+
decision: args.decision,
|
|
228
|
+
scope: args.scope === 'hunk' ? 'block' : 'file',
|
|
229
|
+
...(args.blockId === undefined ? {} : { blockId: args.blockId }),
|
|
230
|
+
})
|
|
231
|
+
if (!result.ok) return { ok: false, error: result.reason }
|
|
232
|
+
return { ok: true, revision: result.flags.revision }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (args.decision === 'rejected') {
|
|
236
|
+
if (!scope.live) return { ok: false, error: UNDO_NEEDS_LIVE }
|
|
237
|
+
const result = await undoReview(ctx, sessionOf(ctx, args.sessionId), scope.cwd, args)
|
|
238
|
+
return { ok: true, ...result }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const result = decideReview(scope.cwd, args)
|
|
242
|
+
if (!scope.live) log.info(`[review] 冷端 Keep 官方记录 ${args.id}`)
|
|
142
243
|
return { ok: true, ...result }
|
|
143
244
|
} catch (error) {
|
|
144
245
|
// Guard failures are review results, not transport failures: preserve their reason.
|
|
145
|
-
|
|
246
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
247
|
+
log.warn(`[review] decide 失败 id=${args.id} decision=${args.decision}: ${message}`)
|
|
248
|
+
return { ok: false, error: message }
|
|
146
249
|
}
|
|
147
250
|
},
|
|
148
251
|
}
|
package/src/rpc.ts
CHANGED
|
@@ -63,7 +63,7 @@ import { settingsDocPath } from './settingsDocPath.js'
|
|
|
63
63
|
import { createTextPages } from './textPages.js'
|
|
64
64
|
import { reviewHandlers } from './reviewRpc.js'
|
|
65
65
|
import { pendingHandlers } from './pendingRpc.js'
|
|
66
|
-
import { fileAbsent, missingReview, missingDecide, missingPath } from './missingReview.js'
|
|
66
|
+
import { fileAbsent, missingReview, missingDecide, missingPath, refusalCodeOf } from './missingReview.js'
|
|
67
67
|
import { pendingCovers } from './pendingCompat.js'
|
|
68
68
|
|
|
69
69
|
/** Process-local cursor signer; reads must stay on the Host filesystem capability. */
|
|
@@ -733,7 +733,11 @@ export function buildHandlers(
|
|
|
733
733
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
734
734
|
if (!await fileAbsent(ctx, sc.cwd, args.path)) return { ok: true, review: null }
|
|
735
735
|
return { ok: true, review: await missingReview(ctx, sc.session, sc.cwd, bucket, args.path) }
|
|
736
|
-
} catch (error) {
|
|
736
|
+
} catch (error) {
|
|
737
|
+
// 可预期的拒绝带稳定错误码:客户端据此显示「已删除」终态,而不是把错误原文当加载失败重试
|
|
738
|
+
const code = refusalCodeOf(error)
|
|
739
|
+
return { ok: false, error: String(error), ...(code ? { code } : {}) }
|
|
740
|
+
}
|
|
737
741
|
},
|
|
738
742
|
'edrv.missingDecide': async (args) => {
|
|
739
743
|
const sc = await requireSession(ctx, args.sessionId)
|
package/src/shared/review.ts
CHANGED
|
@@ -56,7 +56,8 @@ export interface ReviewDetail {
|
|
|
56
56
|
diff: OfficialDiff
|
|
57
57
|
capturedAt: string
|
|
58
58
|
blocks: import('./reviewRegions.js').ReviewBlockState[]
|
|
59
|
-
|
|
59
|
+
/** live 缺省视为未知:只有 host 明确给出 false 时才代表会话不在本端活跃。 */
|
|
60
|
+
capability: { keep: boolean; undo: boolean; reason: string; live?: boolean }
|
|
60
61
|
}
|
|
61
62
|
export interface CaptureGap {
|
|
62
63
|
sessionId: string
|
package/src/shared/rpc.ts
CHANGED
|
@@ -736,10 +736,13 @@ export interface RpcOkMap {
|
|
|
736
736
|
'edrv.uiState.set': object
|
|
737
737
|
}
|
|
738
738
|
|
|
739
|
+
/** 缺失文件预览的稳定拒绝码:客户端据此显示「已删除」终态,而不是把错误原文当加载失败重试。 */
|
|
740
|
+
export type MissingRefusal = 'official-covered' | 'official-decided' | 'no-records' | 'no-evidence' | 'too-large'
|
|
741
|
+
|
|
739
742
|
/** 统一响应:{ok:true, ...payload} 或 {ok:false, error}。 */
|
|
740
743
|
export type RpcResult<M extends RpcMethod> =
|
|
741
744
|
| ({ ok: true } & RpcOkMap[M])
|
|
742
|
-
| { ok: false; error: string; code?: 'file-too-large' | 'file-missing'; reason?: Extract<import('./textPages.js').TextPageResult, { ok: false }>['reason'] }
|
|
745
|
+
| { ok: false; error: string; code?: 'file-too-large' | 'file-missing' | MissingRefusal; reason?: Extract<import('./textPages.js').TextPageResult, { ok: false }>['reason'] }
|
|
743
746
|
|
|
744
747
|
/** 每个方法的 handler 签名(host 分发表与 client 类型检查共用)。 */
|
|
745
748
|
export type RpcHandler<M extends RpcMethod> = (
|