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.
@@ -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
- return { ok: true, page: reviewPage(cwd, args.cursor, args.history, args.path) }
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 { fileAbsent, missingReview, missingDecide } from './missingReview.js'
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 results = await applyDecisions(ctx, sc.session, sc.cwd, bucket, Array.isArray(args.items) ? args.items : [])
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')