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.
Files changed (42) hide show
  1. package/README.md +25 -11
  2. package/lib/client.js +1969 -1344
  3. package/lib/client.js.map +2 -2
  4. package/lib/index.js +1443 -716
  5. package/lib/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/client/compatSummary.ts +4 -0
  8. package/src/client/contextMenusOfficial.ts +80 -0
  9. package/src/client/forgeBridge.ts +28 -0
  10. package/src/client/index.ts +18 -3
  11. package/src/client/monaco/lsp/lspClient.ts +25 -7
  12. package/src/client/officialSidebar.ts +15 -3
  13. package/src/client/reviewBar.ts +3 -0
  14. package/src/client/reviewBatch.ts +9 -1
  15. package/src/client/reviewKeepAll.ts +72 -0
  16. package/src/client/sidebar/menuItems.ts +6 -0
  17. package/src/client/sidebar/panels/FileExplorer.ts +26 -1
  18. package/src/client/sidebar/types.ts +3 -0
  19. package/src/client/state/diffSummary.ts +34 -7
  20. package/src/client/state/recordsFeed.ts +40 -3
  21. package/src/client/state/reviewFeed.ts +9 -2
  22. package/src/client/tabMenu.ts +5 -0
  23. package/src/client/ui/ClaimRouter.ts +11 -5
  24. package/src/client/ui/EditorPane.ts +9 -1
  25. package/src/client/ui/EditorView.ts +164 -39
  26. package/src/client/ui/OfficialSideTab.ts +13 -5
  27. package/src/client/ui/ReviewAllConfirm.ts +1 -1
  28. package/src/index.ts +1 -1
  29. package/src/pending/capture.ts +96 -47
  30. package/src/pending/persist.ts +94 -5
  31. package/src/pending/store.ts +171 -58
  32. package/src/pendingCompat.ts +72 -9
  33. package/src/pendingRpc.ts +10 -10
  34. package/src/reviewCapture.ts +64 -23
  35. package/src/reviewDb.ts +119 -8
  36. package/src/reviewEvidence.ts +1 -1
  37. package/src/reviewRpc.ts +115 -9
  38. package/src/rpc.ts +14 -10
  39. package/src/shared/review.ts +21 -0
  40. package/src/shared/rpc.ts +3 -1
  41. package/src/store.ts +44 -0
  42. package/src/toolTargets.ts +63 -0
@@ -2,15 +2,32 @@
2
2
  * 待审差异采集:环绕一次文件写入读取真实前后内容,只有内容确实变化才落库。
3
3
  * @author ddj 2026年10月29号
4
4
  */
5
- import { cwdOf } from '../registry.js'
6
- import { resolveTarget } from '../store.js'
5
+ import { cwdOfSession } from '../store.js'
7
6
  import type { Ctx } from '../store.js'
8
7
  import { log } from '../log.js'
8
+ import { toolTargets, patchDiagnostics } from '../toolTargets.js'
9
+ import { rawText, rawReviewFile } from '../reviewEvidence.js'
9
10
 
10
11
  /** 可被采信为待审来源的写文件工具。 */
11
- const WRITE_TOOLS = ['edit', 'write', 'str_replace_editor']
12
+ const WRITE_TOOLS = ['edit', 'write', 'str_replace_editor', 'apply_patch']
12
13
  /** 单文件文本上限:超过即标注 oversized,不静默截断。 */
13
14
  const MAX_TEXT = 1024 * 1024
15
+ const captureGaps = new Map<string, { cwd: string; sessionId: string; message: string }>()
16
+
17
+ /** @public @author ddj 2026年11月04号 @param cwd Workspace identity. @returns Bounded runtime-only capture diagnostics. */
18
+ export function pendingGaps(cwd: string): string[] {
19
+ return [...captureGaps.values()].filter((gap) => gap.cwd === cwd).map((gap) => gap.message)
20
+ }
21
+
22
+ /** @private @author ddj 2026年11月04号 @param ctx Host scope. @param exec Original call. @param path Failed target. @param error Evidence failure. Records no writable state. */
23
+ function recordGap(scope: { cwd: string; sessionId: string }, exec: WriteExec, path: string, error: unknown): void {
24
+ const { cwd, sessionId } = scope
25
+ const key = JSON.stringify([cwd, sessionId, exec.id, path])
26
+ captureGaps.set(key, { cwd, sessionId, message: path + ': ' + String(error) })
27
+ const local = [...captureGaps].filter(([, gap]) => gap.cwd === cwd)
28
+ while (local.length > 50) captureGaps.delete(local.shift()![0])
29
+ while (captureGaps.size > 500) captureGaps.delete(captureGaps.keys().next().value!)
30
+ }
14
31
 
15
32
  /** 一次已确认的写入。 */
16
33
  export interface PendingWrite {
@@ -33,12 +50,14 @@ export interface PendingCapture {
33
50
  interface CaptureState {
34
51
  busy: Set<string>
35
52
  stopped: boolean
53
+ disposed: Set<string>
36
54
  }
37
55
 
38
56
  /** 工具的写工具参数形状;只取本采集关心的字段。 */
39
57
  interface WriteExec {
58
+ id?: string
40
59
  name?: string
41
- arguments?: { file_path?: unknown; path?: unknown }
60
+ arguments?: { file_path?: unknown; path?: unknown; input?: unknown }
42
61
  agent?: { session?: { id?: string; cwd?: string } }
43
62
  }
44
63
 
@@ -50,12 +69,19 @@ interface WriteExec {
50
69
  * @returns 可释放的采集器
51
70
  */
52
71
  export function installPending(ctx: Ctx, sink: (write: PendingWrite) => void): PendingCapture {
53
- const state: CaptureState = { busy: new Set<string>(), stopped: false }
72
+ const state: CaptureState = { busy: new Set<string>(), stopped: false, disposed: new Set<string>() }
54
73
  ctx.on('tools/execute', (exec: WriteExec, next: () => Promise<unknown>) => aroundTool(ctx, state, sink, exec, next))
74
+ ctx.on('session/disposed', (session: { id: string }) => {
75
+ state.disposed.add(session.id)
76
+ for (const [key, gap] of captureGaps) {
77
+ if (gap.sessionId === session.id) captureGaps.delete(key)
78
+ }
79
+ })
55
80
  return {
56
81
  dispose(): void {
57
82
  state.stopped = true
58
83
  state.busy.clear()
84
+ captureGaps.clear()
59
85
  },
60
86
  }
61
87
  }
@@ -68,20 +94,24 @@ export function installPending(ctx: Ctx, sink: (write: PendingWrite) => void): P
68
94
  * @param exec 工具分发上下文
69
95
  * @returns 采集计划或 null
70
96
  */
71
- async function planOf(ctx: Ctx, state: CaptureState, exec: WriteExec): Promise<CapturePlan | null> {
72
- const session = exec.agent?.session
73
- const requested = exec.arguments?.file_path ?? exec.arguments?.path
74
- if (state.stopped || !session?.id || typeof requested !== 'string') return null
75
- if (!WRITE_TOOLS.includes(String(exec.name))) return null
76
- const cwd = cwdOf(session as never)
77
- if (!cwd) return null
78
- const fs = ctx.get('fs')
79
- const target = await resolveTarget(ctx, session as never, requested)
97
+ async function planOf(scope: { cwd: string; sessionId: string; fs: any }, state: CaptureState, requested: string): Promise<CapturePlan | null> {
98
+ const { cwd, sessionId, fs } = scope
99
+ if (state.stopped || state.disposed.has(sessionId)) return null
100
+ const target = await fs.resolve(requested, { cwd })
80
101
  const identity = fs.processPath(target)
81
- if (typeof identity !== 'string' || !identity || state.busy.has(identity)) return null
82
- const before = await readText(fs, target)
83
- if (before !== null && before.length > MAX_TEXT) return null
84
- return { cwd: String(cwd), sessionId: String(session.id), path: requested, identity, before, session: session as never }
102
+ if (state.stopped || state.disposed.has(sessionId) || typeof identity !== 'string' || !identity || state.busy.has(identity)) return null
103
+ state.busy.add(identity)
104
+ try {
105
+ const before = await readText(fs, target)
106
+ if (state.stopped || state.disposed.has(sessionId) || (before !== null && before.length > MAX_TEXT)) {
107
+ state.busy.delete(identity)
108
+ return null
109
+ }
110
+ return { cwd: String(cwd), sessionId, path: requested, identity, before, target, fs }
111
+ } catch (error) {
112
+ state.busy.delete(identity)
113
+ throw error
114
+ }
85
115
  }
86
116
 
87
117
  /** 一次采集所需的固定信息。 */
@@ -91,7 +121,8 @@ interface CapturePlan {
91
121
  path: string
92
122
  identity: string
93
123
  before: string | null
94
- session: never
124
+ target: unknown
125
+ fs: any
95
126
  }
96
127
 
97
128
  /**
@@ -108,58 +139,76 @@ async function aroundTool(
108
139
  ctx: Ctx, state: CaptureState, sink: (write: PendingWrite) => void,
109
140
  exec: WriteExec, next: () => Promise<unknown>,
110
141
  ): Promise<unknown> {
111
- let plan: CapturePlan | null = null
142
+ if (state.stopped || !WRITE_TOOLS.includes(String(exec.name))) return next()
143
+ const session = exec.agent?.session
144
+ const cwd = session ? cwdOfSession(ctx, session as never) : null
145
+ if (!session?.id || !cwd) return next()
146
+ const scope = { cwd, sessionId: session.id, fs: ctx.get('fs') }
147
+ const plans: CapturePlan[] = []
112
148
  try {
113
- plan = await planOf(ctx, state, exec)
114
- } catch (error) {
115
- log.debug('待审采集跳过:' + String(error))
116
- }
117
- if (!plan) return next()
149
+ const paths = await toolTargets(exec)
150
+ for (const path of paths) {
151
+ try {
152
+ if (plans.length >= 16) throw new Error('capture target budget exceeded')
153
+ const plan = await planOf(scope, state, path)
154
+ if (plan) plans.push(plan)
155
+ } catch (error) { if (!state.stopped && !state.disposed.has(scope.sessionId)) recordGap(scope, exec, path, error); log.debug('待审采集跳过:' + path + ': ' + String(error)) }
156
+ }
157
+ } catch (error) { if (!state.stopped && !state.disposed.has(scope.sessionId)) recordGap(scope, exec, String(exec.name), error); log.debug('待审目标不可用:' + String(error)) }
118
158
 
119
- state.busy.add(plan.identity)
120
159
  try {
121
- return await next()
160
+ const result = await next()
161
+ if (exec.name === 'apply_patch' && !state.stopped && !state.disposed.has(scope.sessionId)) {
162
+ for (const diagnostic of patchDiagnostics(result)) recordGap(scope, exec, 'apply_patch', diagnostic)
163
+ }
164
+ return result
122
165
  } finally {
123
- state.busy.delete(plan.identity)
124
- try {
125
- reportWrite(ctx, sink, plan)
126
- } catch (error) {
127
- log.debug('待审上报失败:' + String(error))
166
+ for (const plan of plans) {
167
+ try { await reportWrite(state, sink, plan) }
168
+ catch (error) { if (!state.stopped && !state.disposed.has(scope.sessionId)) recordGap(scope, exec, plan.path, error); log.debug('待审上报失败:' + String(error)) }
169
+ finally { state.busy.delete(plan.identity) }
128
170
  }
129
171
  }
130
172
  }
131
173
 
132
174
  /**
133
- * @author ddj 2026年10月29号
134
- * @description: 写后读回并比较,内容一致视为无净变化不产生条目。
135
- * @param ctx Host 生命周期
175
+ * @author ddj 2026年11月04号
176
+ * @description: 固定目标写后读回;内容一致不产生条目,卸载后不再上报。
177
+ * @param state 采集生命周期状态
136
178
  * @param sink 已确认写入的接收方
137
179
  * @param plan 采集计划
138
180
  * @returns 无;读回失败只记日志
139
181
  */
140
- function reportWrite(ctx: Ctx, sink: (write: PendingWrite) => void, plan: CapturePlan): void {
141
- const fs = ctx.get('fs')
142
- void resolveTarget(ctx, plan.session, plan.path).then(async (target: unknown) => {
143
- const after = await readText(fs, target)
144
- if (after !== null && after.length > MAX_TEXT) return
145
- if (plan.before === after) return
146
- sink({ cwd: plan.cwd, sessionId: plan.sessionId, path: plan.path, identity: plan.identity, before: plan.before, after, now: Date.now() })
147
- }).catch((error: unknown) => log.debug('待审读回失败:' + String(error)))
182
+ async function reportWrite(state: CaptureState, sink: (write: PendingWrite) => void, plan: CapturePlan): Promise<void> {
183
+ if (state.stopped || state.disposed.has(plan.sessionId)) return
184
+ // 目录、provider 和目标均在写前冻结,切目录或卸载不能将内容串到其他工作区。
185
+ const after = await readText(plan.fs, plan.target)
186
+ if (state.stopped || state.disposed.has(plan.sessionId)) return
187
+ if (after !== null && after.length > MAX_TEXT) throw new Error('capture text budget exceeded')
188
+ for (const [key, gap] of captureGaps) {
189
+ const identity = JSON.parse(key) as unknown[]
190
+ if (gap.cwd === plan.cwd && gap.sessionId === plan.sessionId && identity[3] === plan.path) captureGaps.delete(key)
191
+ }
192
+ if (plan.before === after) return
193
+ sink({ cwd: plan.cwd, sessionId: plan.sessionId, path: plan.path, identity: plan.identity, before: plan.before, after, now: Date.now() })
148
194
  }
149
195
 
150
196
  /**
151
197
  * @author ddj 2026年10月29号
152
- * @description: 读取全文;缺失或不可读按"无内容"处理,交由上层决定语义。
198
+ * @description: 读取全文;只有明确缺失返回 null,不可读或超限错误保留失败语义。
153
199
  * @param fs 文件系统
154
200
  * @param target 目标
155
201
  * @returns 文本或 null
156
202
  */
157
- export async function readText(fs: any, target: any): Promise<string | null> {
203
+ export async function readText(fs: any, target: any, signal?: AbortSignal): Promise<string | null> {
158
204
  try {
159
- return String(await fs.readText(target))
205
+ if (typeof fs.readBytes === 'function') {
206
+ return rawText(rawReviewFile(await fs.readBytes(target, signal, MAX_TEXT)))
207
+ }
208
+ throw new Error('Raw byte capture unavailable')
160
209
  } catch (error) {
161
210
  const code = (error as { code?: string }).code
162
- if (code === 'ENOENT' || code === 'FS_NOT_FOUND' || code === 'FS_TOO_LARGE') return null
211
+ if (code === 'ENOENT' || code === 'FS_NOT_FOUND') return null
163
212
  throw error
164
213
  }
165
214
  }
@@ -107,8 +107,20 @@ export function loadEntries(cwd: string): StoredEntry[] {
107
107
  * @returns 无
108
108
  */
109
109
  export function saveEntry(cwd: string, stored: StoredEntry): void {
110
+ upsertEntry(pendingDb(cwd), stored)
111
+ }
112
+
113
+ /**
114
+ * @private
115
+ * @author ddj 2026年11月03号
116
+ * @description: 在给定连接上写入或更新一条条目(含标记),供单条与批量两个入口复用。
117
+ * @param db 已迁移连接(可能已在事务中)
118
+ * @param stored 条目与标记
119
+ * @returns 无
120
+ */
121
+ function upsertEntry(db: DatabaseSync, stored: StoredEntry): void {
110
122
  const { entry, flags } = stored
111
- pendingDb(cwd).prepare(`INSERT INTO pending_entries
123
+ db.prepare(`INSERT INTO pending_entries
112
124
  (id,path,earlier,old_text,new_text,updated_at,unseen,missing,diverged,oversized,revision,session_id,session_ids,lineage)
113
125
  VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
114
126
  ON CONFLICT(id) DO UPDATE SET path=excluded.path,earlier=excluded.earlier,old_text=excluded.old_text,
@@ -165,9 +177,7 @@ export function saveBlocks(cwd: string, entryId: string, blocks: { id: string; d
165
177
  const db = pendingDb(cwd)
166
178
  db.exec('BEGIN IMMEDIATE')
167
179
  try {
168
- db.prepare('DELETE FROM pending_blocks WHERE entry_id=?').run(entryId)
169
- const insert = db.prepare('INSERT INTO pending_blocks VALUES(?,?,?)')
170
- for (const block of blocks) insert.run(entryId, block.id, block.decision)
180
+ writeBlocks(db, entryId, blocks)
171
181
  db.exec('COMMIT')
172
182
  } catch (error) {
173
183
  if (db.isTransaction) db.exec('ROLLBACK')
@@ -175,6 +185,21 @@ export function saveBlocks(cwd: string, entryId: string, blocks: { id: string; d
175
185
  }
176
186
  }
177
187
 
188
+ /**
189
+ * @private
190
+ * @author ddj 2026年11月03号
191
+ * @description: 在给定连接上覆盖某条目的块决定集合,供单条与批量两个入口复用。
192
+ * @param db 已迁移连接(可能已在事务中)
193
+ * @param entryId 条目身份
194
+ * @param blocks 块决定
195
+ * @returns 无
196
+ */
197
+ function writeBlocks(db: DatabaseSync, entryId: string, blocks: { id: string; decision: string }[]): void {
198
+ db.prepare('DELETE FROM pending_blocks WHERE entry_id=?').run(entryId)
199
+ const insert = db.prepare('INSERT INTO pending_blocks VALUES(?,?,?)')
200
+ for (const block of blocks) insert.run(entryId, block.id, block.decision)
201
+ }
202
+
178
203
 
179
204
  /**
180
205
  * @author ddj 2026年10月29号
@@ -222,7 +247,21 @@ export function writeOp(
222
247
  * @returns 无
223
248
  */
224
249
  export function pushHistory(cwd: string, kind: string, entryId: string, snapshot: string, now: number): void {
225
- const db = pendingDb(cwd)
250
+ insertHistory(pendingDb(cwd), kind, entryId, snapshot, now)
251
+ }
252
+
253
+ /**
254
+ * @private
255
+ * @author ddj 2026年11月03号
256
+ * @description: 在给定连接上追加撤销栈记录并截断重做分支,供单条与批量两个入口复用。
257
+ * @param db 已迁移连接(可能已在事务中)
258
+ * @param kind 操作种类
259
+ * @param entryId 涉及的条目
260
+ * @param snapshot 可重放快照
261
+ * @param now 时间
262
+ * @returns 无
263
+ */
264
+ function insertHistory(db: DatabaseSync, kind: string, entryId: string, snapshot: string, now: number): void {
226
265
  db.prepare("DELETE FROM pending_history WHERE state='undone'").run()
227
266
  db.prepare("INSERT INTO pending_history(kind,entry_id,snapshot,created_at,state) VALUES(?,?,?,?,'applied')")
228
267
  .run(kind, entryId, snapshot, now)
@@ -327,3 +366,53 @@ function flagsOfRow(row: Row): PendingFlags {
327
366
  revision: Number(row.revision),
328
367
  }
329
368
  }
369
+
370
+ /** 批量 Keep 的一条落库记录:stored 提供 CAS 依据与撤销快照 before,next 是推进后的条目。 */
371
+ export interface KeepRecord {
372
+ entryId: string
373
+ stored: StoredEntry
374
+ next: StoredEntry
375
+ blocks: { id: string; decision: string }[]
376
+ snapshot: string
377
+ }
378
+
379
+ /**
380
+ * @public
381
+ * @author ddj 2026年11月03号
382
+ * @description: 在一次事务内批量推进多条待审条目(批量 Keep 用)。每条按 revision 做 CAS,
383
+ * 只跳过冲突的那一条,不牵连整批;块决定与撤销快照与单条路径同口径。
384
+ * @param cwd 工作区
385
+ * @param records 待落库记录
386
+ * @returns 因版本已变被跳过的条目
387
+ * @throws 事务级异常(SQL/程序错误)整体回滚后抛出
388
+ */
389
+ export function saveDecisions(cwd: string, records: KeepRecord[]): { id: string; reason: string }[] {
390
+ const db = pendingDb(cwd)
391
+ const skipped: { id: string; reason: string }[] = []
392
+ const advance = db.prepare(`UPDATE pending_entries SET path=?,earlier=?,old_text=?,new_text=?,updated_at=?,unseen=?,
393
+ missing=?,diverged=?,oversized=?,revision=?,session_id=?,session_ids=?,lineage=? WHERE id=? AND revision=?`)
394
+
395
+ db.exec('BEGIN IMMEDIATE')
396
+ try {
397
+ for (const record of records) {
398
+ const { entry, flags } = record.next
399
+ const expected = record.stored.flags.revision
400
+ const changed = advance.run(entry.path, entry.earlierVersion, entry.oldText, entry.newText, entry.updatedAt,
401
+ entry.unseen === true ? 1 : 0, flags.missing ? 1 : 0, flags.diverged ? 1 : 0, flags.oversized ? 1 : 0,
402
+ flags.revision, entry.sessionId, JSON.stringify(entry.sessionIds),
403
+ entry.lineage ? JSON.stringify(entry.lineage) : null, record.entryId, expected)
404
+ // 读条目到落库之间被别的决定推进过:只放弃这一条
405
+ if (changed.changes !== 1) {
406
+ skipped.push({ id: record.entryId, reason: '待审条目已变化,请重试' })
407
+ continue
408
+ }
409
+ writeBlocks(db, record.entryId, record.blocks)
410
+ insertHistory(db, 'decide', record.entryId, record.snapshot, Date.now())
411
+ }
412
+ db.exec('COMMIT')
413
+ return skipped
414
+ } catch (error) {
415
+ if (db.isTransaction) db.exec('ROLLBACK')
416
+ throw error
417
+ }
418
+ }