dsh-vscode-mode 0.14.2 → 0.14.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 +9 -8
- package/lib/client.js +412 -209
- package/lib/client.js.map +1 -1
- package/lib/index.js +1284 -486
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/archiveDb.ts +476 -0
- package/src/capture.ts +8 -1
- package/src/client/diffDockStore.ts +13 -0
- package/src/client/monaco/diffRender.ts +3 -0
- package/src/client/state/regions.ts +21 -1
- package/src/client/styles/editor.css +12 -0
- package/src/client/ui/ConversationDiffDock.ts +2 -2
- package/src/client/ui/DiffLauncher.ts +76 -17
- package/src/client/ui/EditorView.ts +90 -7
- package/src/index.ts +2 -0
- package/src/paths.ts +11 -0
- package/src/perf.ts +17 -24
- package/src/revert.ts +93 -1
- package/src/rpc.ts +241 -67
- package/src/shared/diff.ts +69 -20
- package/src/shared/rpc.ts +8 -1
- package/src/store.ts +154 -55
package/src/store.ts
CHANGED
|
@@ -3,17 +3,21 @@
|
|
|
3
3
|
* 迁移自原 src/index.ts 的 fs/ctx IO 部分,语义一字不改。
|
|
4
4
|
* 作者 ddj 2026-08-20
|
|
5
5
|
*/
|
|
6
|
-
import type { ArchiveBatch,
|
|
6
|
+
import type { ArchiveBatch, DiffRecord, SidecarData } from './shared/types.js'
|
|
7
7
|
import type { ReadState } from './shared/diff.js'
|
|
8
8
|
import { fingerprint, isNoopHunk, locateHunks, normalizeForCompare, normalizeHunk, preciseHunk } from './shared/diff.js'
|
|
9
|
-
import { archiveEntryFor,
|
|
9
|
+
import { archiveEntryFor, normalizeRecord } from './model.js'
|
|
10
10
|
import { SIDECAR, SIDECAR_ARCHIVE } from './paths.js'
|
|
11
11
|
import { log } from './log.js'
|
|
12
|
+
import { debugRecord } from './debugLog.js'
|
|
13
|
+
import { activeReady, appendArchive, checkActiveSource, commitActive, committedRecord, migrateActive, restoreReverts, type ActiveChange } from './archiveDb.js'
|
|
12
14
|
|
|
13
15
|
export { SIDECAR, SIDECAR_ARCHIVE }
|
|
14
16
|
export const READ_CAP = 8 * 1024 * 1024
|
|
15
17
|
/** 二进制预览/写回上限(PDF 等整文件 base64 传输;文本侧仍用 READ_CAP)。 */
|
|
16
18
|
export const BINARY_READ_CAP = 32 * 1024 * 1024
|
|
19
|
+
/** 旧文件首次全量指纹核验后,每次操作优先检查 DSH fs stat 身份。 */
|
|
20
|
+
const sourceStats = new Map<string, string>()
|
|
17
21
|
|
|
18
22
|
/** DSH 上下文与会话类型较宽松:本地无 dsh 类型声明,显式 any + 文档约束。 */
|
|
19
23
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -76,33 +80,88 @@ export function parseSidecar(text: string | null): SidecarData | null {
|
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* 严格读取旧 sidecar:仅确认确实不存在时才允许从空记录迁移。
|
|
85
|
+
* @author ddj 2026年09月30号
|
|
86
|
+
* @param ctx DSH 上下文
|
|
87
|
+
* @param cwd 工作区
|
|
88
|
+
* @returns 旧文件内容或确认缺失
|
|
89
|
+
*/
|
|
90
|
+
async function migrationText(ctx: Ctx, cwd: string): Promise<string | null> {
|
|
91
|
+
const fs = ctx.get('fs')
|
|
92
|
+
if (!fs) throw new Error('无法迁移差异记录:缺少 fs')
|
|
93
|
+
const target = await fs.resolve(SIDECAR, { cwd })
|
|
94
|
+
const info = await fs.stat(target)
|
|
95
|
+
if (!info) return null
|
|
96
|
+
if (info.type !== 'file') throw new Error('旧活跃 sidecar 非文件')
|
|
97
|
+
return fs.readText(target)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 读取旧 sidecar 文件的版本身份;缺少可靠版本令牌时返回 null 走全文核验。
|
|
102
|
+
* @author ddj 2026年09月30号
|
|
103
|
+
* @param ctx DSH 上下文
|
|
104
|
+
* @param cwd 工作区
|
|
105
|
+
* @returns 文件身份或 null
|
|
106
|
+
*/
|
|
107
|
+
async function sourceStat(ctx: Ctx, cwd: string): Promise<string | null> {
|
|
108
|
+
const fs = ctx.get('fs')
|
|
109
|
+
if (!fs) throw new Error('无法核验旧 sidecar:缺少 fs')
|
|
110
|
+
const info = await fs.stat(await fs.resolve(SIDECAR, { cwd }))
|
|
111
|
+
if (!info) return 'absent'
|
|
112
|
+
const version = info.version ?? info.rev
|
|
113
|
+
return typeof version === 'string' && version.length ? version : null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 首次读取旧文件全文确认数据库来源,后续只在 stat 版本变动时重新核验。
|
|
118
|
+
* @author ddj 2026年09月30号
|
|
119
|
+
* @param ctx DSH 上下文
|
|
120
|
+
* @param cwd 工作区
|
|
121
|
+
*/
|
|
122
|
+
async function checkSource(ctx: Ctx, cwd: string): Promise<void> {
|
|
123
|
+
const now = await sourceStat(ctx, cwd)
|
|
124
|
+
if (now !== null && sourceStats.get(cwd) === now) return
|
|
125
|
+
checkActiveSource(cwd, await migrationText(ctx, cwd))
|
|
126
|
+
if (now !== null) sourceStats.set(cwd, now)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 加载某工作区的记录桶(内存 Map;首次在隔离切换窗口严格迁移)。 */
|
|
80
130
|
export async function loadBucket(ctx: Ctx, cwd: string): Promise<Map<string, DiffRecord>> {
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
131
|
+
const source = await migrationText(ctx, cwd)
|
|
132
|
+
const map = await migrateActive(cwd, source, () => migrationText(ctx, cwd))
|
|
133
|
+
await checkSource(ctx, cwd)
|
|
134
|
+
for (const rec of map.values()) {
|
|
135
|
+
if (rec.archived) continue
|
|
136
|
+
const previous = JSON.stringify(rec)
|
|
137
|
+
restoreReverts(cwd, rec)
|
|
138
|
+
const committed = committedRecord(cwd, rec.callId, rec.path, rec.batch ?? null)
|
|
139
|
+
if (committed) {
|
|
140
|
+
rec.decisions = committed.decisions
|
|
141
|
+
rec.superseded = committed.superseded
|
|
142
|
+
rec.archived = true
|
|
88
143
|
}
|
|
144
|
+
if (JSON.stringify(rec) !== previous) commitActive(cwd, { upserts: [rec], removed: [] })
|
|
89
145
|
}
|
|
90
146
|
return map
|
|
91
147
|
}
|
|
92
148
|
|
|
93
|
-
/**
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
149
|
+
/**
|
|
150
|
+
* 仅把明确变动的记录行写入 SQLite;旧 sidecar 不再写入。
|
|
151
|
+
* @author ddj 2026年09月30号
|
|
152
|
+
* @param ctx DSH 上下文
|
|
153
|
+
* @param cwd 工作区
|
|
154
|
+
* @param recsMap 供当前调用点核对的记录桶
|
|
155
|
+
* @param session 会话(保留旧 API)
|
|
156
|
+
* @param changes 明确的更新和删除集合
|
|
157
|
+
*/
|
|
158
|
+
export async function saveBucket(ctx: Ctx, cwd: string, recsMap: Map<string, DiffRecord>, session?: Session, changes?: ActiveChange): Promise<void> {
|
|
159
|
+
if (!changes) throw new Error('增量保存缺少变更集合')
|
|
160
|
+
for (const rec of changes.upserts) if (recsMap.get(rec.callId) !== rec) throw new Error('增量记录不在工作区桶内')
|
|
97
161
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
existing.updatedAt = new Date().toISOString()
|
|
102
|
-
await fs.writeText(target, JSON.stringify(existing), void 0, void 0, policyOf(ctx, session))
|
|
103
|
-
} catch (error) {
|
|
104
|
-
log.error('saveBucket failed: ' + String(error))
|
|
105
|
-
}
|
|
162
|
+
await checkSource(ctx, cwd)
|
|
163
|
+
commitActive(cwd, changes)
|
|
164
|
+
} catch (error) { log.error('saveBucket failed: ' + String(error)); throw error }
|
|
106
165
|
}
|
|
107
166
|
|
|
108
167
|
/**
|
|
@@ -172,8 +231,18 @@ export async function autoArchiveStale(ctx: Ctx, session: Session, cwd: string,
|
|
|
172
231
|
if (await recordIsStale(ctx, session, cache, rec)) stale.push(rec)
|
|
173
232
|
}
|
|
174
233
|
if (!stale.length) return 0
|
|
234
|
+
const original = stale.map((rec) => ({ rec, at: rec.at, superseded: rec.superseded }))
|
|
175
235
|
for (const r of stale) { r.superseded = true; r.at = new Date().toISOString() }
|
|
176
|
-
|
|
236
|
+
try {
|
|
237
|
+
await archiveRecords(ctx, session, cwd, bucket, stale, '差异无法定位(已被后续修改覆盖),自动归档')
|
|
238
|
+
} catch (error) {
|
|
239
|
+
for (const item of original) {
|
|
240
|
+
if (item.rec.archived) continue
|
|
241
|
+
item.rec.superseded = item.superseded
|
|
242
|
+
item.rec.at = item.at
|
|
243
|
+
}
|
|
244
|
+
throw error
|
|
245
|
+
}
|
|
177
246
|
return stale.length
|
|
178
247
|
}
|
|
179
248
|
|
|
@@ -198,40 +267,49 @@ export function parseArchive(text: string | null): ArchiveBatch[] {
|
|
|
198
267
|
return []
|
|
199
268
|
}
|
|
200
269
|
|
|
201
|
-
/**
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
270
|
+
/**
|
|
271
|
+
* 追加归档条目(按 cwd+path+batch 合并,同 callId 去重)。
|
|
272
|
+
* @author ddj 2026年09月30号
|
|
273
|
+
* @param ctx DSH 上下文
|
|
274
|
+
* @param cwd 工作区
|
|
275
|
+
* @param entries 待归档批次
|
|
276
|
+
* @param session 会话
|
|
277
|
+
* @param traceId 可选诊断追踪标识;有值时输出分段耗时
|
|
278
|
+
*/
|
|
279
|
+
export async function appendArchiveEntries(ctx: Ctx, cwd: string, entries: Array<ReturnType<typeof archiveEntryFor>>, session?: Session, traceId?: string): Promise<void> {
|
|
280
|
+
if (!cwd || !entries.length) return
|
|
281
|
+
// #region debug log
|
|
282
|
+
const started = traceId ? performance.now() : 0
|
|
283
|
+
// #endregion
|
|
205
284
|
try {
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
for (const rec of e.records) {
|
|
214
|
-
if (!seen.has(String(rec.callId))) {
|
|
215
|
-
exist.records.push(rec as unknown as ArchiveBatch['records'][number])
|
|
216
|
-
seen.add(String(rec.callId))
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
exist.lastAt = e.at
|
|
220
|
-
exist.reason = e.reason
|
|
221
|
-
} else {
|
|
222
|
-
existing.push(e as unknown as ArchiveBatch)
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
const data: ArchiveData = { version: 1, updatedAt: new Date().toISOString(), batches: existing }
|
|
226
|
-
await fs.writeText(target, JSON.stringify(data), void 0, void 0, policyOf(ctx, session))
|
|
285
|
+
await checkSource(ctx, cwd)
|
|
286
|
+
const added = appendArchive(cwd, entries as unknown as ArchiveBatch[])
|
|
287
|
+
// #region debug log
|
|
288
|
+
try {
|
|
289
|
+
if (traceId) debugRecord(ctx, cwd, `[DEBUG diffArchive] trace=${traceId} stage=done mode=sqlite entries=${entries.length} insertedBatches=${added} dbMs=${(performance.now() - started).toFixed(1)} totalMs=${(performance.now() - started).toFixed(1)}`)
|
|
290
|
+
} catch { /* 诊断不可影响归档 */ }
|
|
291
|
+
// #endregion
|
|
227
292
|
} catch (error) {
|
|
293
|
+
// #region debug log
|
|
294
|
+
try {
|
|
295
|
+
if (traceId) debugRecord(ctx, cwd, `[DEBUG diffArchive] trace=${traceId} stage=error mode=sqlite totalMs=${(performance.now() - started).toFixed(1)}`)
|
|
296
|
+
} catch { /* 诊断不可影响归档 */ }
|
|
297
|
+
// #endregion
|
|
228
298
|
log.error('appendArchiveEntries failed: ' + String(error))
|
|
299
|
+
throw error
|
|
229
300
|
}
|
|
230
301
|
}
|
|
231
302
|
|
|
232
303
|
/**
|
|
233
304
|
* 归档记录:标记 archived、按批次写入归档、落盘工作区桶。
|
|
234
|
-
* @
|
|
305
|
+
* @author ddj 2026年09月30号
|
|
306
|
+
* @param ctx DSH 上下文
|
|
307
|
+
* @param session 会话
|
|
308
|
+
* @param cwd 工作区
|
|
309
|
+
* @param bucket 当前工作区记录桶
|
|
310
|
+
* @param recs 待归档记录
|
|
311
|
+
* @param reason 归档原因
|
|
312
|
+
* @param opts deferSave 为 true 时只写归档、不落盘桶;traceId 用于可选分段计时
|
|
235
313
|
*/
|
|
236
314
|
export async function archiveRecords(
|
|
237
315
|
ctx: Ctx,
|
|
@@ -240,13 +318,34 @@ export async function archiveRecords(
|
|
|
240
318
|
bucket: Map<string, DiffRecord>,
|
|
241
319
|
recs: DiffRecord[],
|
|
242
320
|
reason: string,
|
|
243
|
-
opts?: { deferSave?: boolean },
|
|
321
|
+
opts?: { deferSave?: boolean; traceId?: string; changed?: DiffRecord[] },
|
|
244
322
|
): Promise<void> {
|
|
245
323
|
if (!recs.length) return
|
|
246
324
|
const fresh = recs.filter((r) => !r.archived)
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
325
|
+
const byFile = new Map<string, DiffRecord[]>()
|
|
326
|
+
for (const rec of recs) {
|
|
327
|
+
const key = JSON.stringify([rec.path, Number.isInteger(rec.batch) ? rec.batch : null])
|
|
328
|
+
const list = byFile.get(key) ?? []
|
|
329
|
+
list.push(rec)
|
|
330
|
+
byFile.set(key, list)
|
|
331
|
+
}
|
|
332
|
+
const entries = [...byFile.values()].map((list) => archiveEntryFor(list, cwd, reason))
|
|
333
|
+
const changed = new Map((opts?.changed ?? []).map((rec) => [rec.callId, rec]))
|
|
334
|
+
for (const rec of recs) changed.set(rec.callId, rec)
|
|
335
|
+
const start = opts?.traceId ? performance.now() : 0
|
|
336
|
+
await checkSource(ctx, cwd)
|
|
337
|
+
const original = recs.map((rec) => rec.archived)
|
|
338
|
+
for (const rec of recs) rec.archived = true
|
|
339
|
+
try {
|
|
340
|
+
const added = commitActive(cwd, { upserts: [...changed.values()], removed: [] }, entries as unknown as ArchiveBatch[])
|
|
341
|
+
// #region debug log
|
|
342
|
+
try {
|
|
343
|
+
if (opts?.traceId) debugRecord(ctx, cwd, `[DEBUG diffArchive] trace=${opts.traceId} stage=done mode=sqlite entries=${entries.length} insertedBatches=${added} dbMs=${(performance.now() - start).toFixed(1)} totalMs=${(performance.now() - start).toFixed(1)}`)
|
|
344
|
+
} catch { /* 诊断不可影响归档 */ }
|
|
345
|
+
// #endregion
|
|
346
|
+
} catch (error) {
|
|
347
|
+
recs.forEach((rec, idx) => { rec.archived = original[idx] })
|
|
348
|
+
throw error
|
|
349
|
+
}
|
|
350
|
+
if (fresh.length && !opts?.deferSave) return
|
|
252
351
|
}
|