dsh-vscode-mode 0.1.29 → 0.1.30
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/lib/client.js +32 -3
- package/lib/client.js.map +1 -1
- package/lib/index.js +72 -14
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/monaco/diffRender.ts +7 -0
- package/src/client/rpc.ts +13 -2
- package/src/client/ui/EditorView.ts +11 -3
- package/src/rpc.ts +79 -19
package/package.json
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
export function createDiffRenderer(log) {
|
|
13
13
|
let viewZoneIds = []
|
|
14
14
|
let decorations = []
|
|
15
|
+
/** 上次已渲染的差异指纹(callId:idx@start-end:oldLen/newLen 串):内容未变时跳过重建。 */
|
|
16
|
+
let lastKey = null
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* 渲染当前 pending 差异:绿底 decoration + 删除块 view zones + '-' 号 overlay(滚动同步)。
|
|
@@ -26,6 +28,11 @@ export function createDiffRenderer(log) {
|
|
|
26
28
|
if (!pendingRegions.length && viewZoneIds.length === 0 && decorations.length === 0) return
|
|
27
29
|
const renderT0 = Date.now()
|
|
28
30
|
const callIdAttr = (callId, idx) => String(callId) + ':' + String(idx)
|
|
31
|
+
// 指纹相同 = 内容未变(轮询/引用空转/重复点击触发的重渲染):残留 DOM 已在位,
|
|
32
|
+
// 直接跳过整轮重建(view zones 拆建 / overlay DOM / 50ms 校正),消除同区域重复渲染成本。
|
|
33
|
+
const key = pendingRegions.map((r) => callIdAttr(r.callId, r.idx) + '@' + (r.start ?? '?') + '-' + (r.end ?? '?') + ':' + (r.oldLines ? r.oldLines.length : 0) + '/' + (r.newLines ? r.newLines.length : 0)).join('|')
|
|
34
|
+
if (key === lastKey) return
|
|
35
|
+
lastKey = key
|
|
29
36
|
log(sessionId, '[diff-render] begin regs=' + pendingRegions.length + ' ' + pendingRegions.map((r) => callIdAttr(r.callId, r.idx) + '@' + (r.start ?? '?') + '-' + (r.end ?? '?') + '(-' + (r.oldLines ? r.oldLines.length : 0) + '/+' + (r.newLines ? r.newLines.length : 0) + ')').join('|'))
|
|
30
37
|
|
|
31
38
|
const decos = []
|
package/src/client/rpc.ts
CHANGED
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
import { RPC_PATH } from '../shared/rpc.js'
|
|
7
7
|
import type { RpcMethod, RpcRequestMap, RpcResult } from '../shared/rpc.js'
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* 诊断日志开关(默认关):diff 渲染热路径每条日志 = 1 次 RPC 往返 +
|
|
11
|
+
* host 端 debug 文件全量读改写(上限 512KB),高频渲染下开销可观。
|
|
12
|
+
* 排查问题时在浏览器控制台执行 localStorage.setItem('edrv.debug','1') 后刷新开启。
|
|
13
|
+
* @author ddj 2026年08月26号
|
|
14
|
+
*/
|
|
15
|
+
const DEBUG = (() => {
|
|
16
|
+
try { return localStorage.getItem('edrv.debug') === '1' } catch (e) { return false }
|
|
17
|
+
})()
|
|
18
|
+
|
|
9
19
|
/** 同源 RPC 调用(host /edrv/rpc 精确路由)。 */
|
|
10
20
|
export function rpc<M extends RpcMethod>(method: M, args: RpcRequestMap[M]): Promise<RpcResult<M>> {
|
|
11
21
|
return fetch(RPC_PATH, {
|
|
@@ -16,12 +26,13 @@ export function rpc<M extends RpcMethod>(method: M, args: RpcRequestMap[M]): Pro
|
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
/**
|
|
19
|
-
*
|
|
20
|
-
* @author ddj 2026年08月
|
|
29
|
+
* 诊断日志:开关关闭时直接返回(零 RPC 零落盘);开启时走 host edrv.debug 落盘。
|
|
30
|
+
* @author ddj 2026年08月26号
|
|
21
31
|
* @param sessionId 会话 id
|
|
22
32
|
* @param text 日志文本
|
|
23
33
|
*/
|
|
24
34
|
export function dbg(sessionId: string | undefined, text: string): void {
|
|
35
|
+
if (!DEBUG) return
|
|
25
36
|
try {
|
|
26
37
|
rpc('edrv.debug', { sessionId, text }).catch(() => {})
|
|
27
38
|
} catch (e) { /* 日志失败忽略 */ }
|
|
@@ -795,7 +795,9 @@ export function EditorView(props) {
|
|
|
795
795
|
fail++
|
|
796
796
|
}
|
|
797
797
|
}
|
|
798
|
-
|
|
798
|
+
// 合并结果无变化(批量全部失败/重复点击)时不 setRecords:避免 records 换引用
|
|
799
|
+
// → pendingRegions 换引用 → diff 全量重渲染(内容相同,纯空转)
|
|
800
|
+
if (Object.keys(next).length) setRecords((prev) => Object.assign({}, prev, next))
|
|
799
801
|
return { ok, fail }
|
|
800
802
|
}).catch((e) => {
|
|
801
803
|
setError('批量处理异常:' + String(e))
|
|
@@ -813,20 +815,26 @@ export function EditorView(props) {
|
|
|
813
815
|
const itemOf = (r, reject) => ({ callId: r.callId, scope: r.create ? 'call' : 'hunk', hunkIndex: r.idx, decision: reject ? 'rejected' : 'accepted' })
|
|
814
816
|
|
|
815
817
|
const acceptFile = () => {
|
|
818
|
+
if (batchBusyRef.current || !pendingRegions.length) return
|
|
819
|
+
batchBusyRef.current = true
|
|
816
820
|
actMany(pendingRegions.map((r) => itemOf(r, false))).then(({ ok, fail }) => {
|
|
821
|
+
batchBusyRef.current = false
|
|
817
822
|
reloadFile(true)
|
|
818
823
|
emitRefresh()
|
|
819
824
|
setStatus('已采纳 ' + ok + ' 处差异' + (fail ? ',' + fail + ' 处失败' : ''))
|
|
820
825
|
if (fail) setError(fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
|
|
821
|
-
})
|
|
826
|
+
}).catch(() => { batchBusyRef.current = false })
|
|
822
827
|
}
|
|
823
828
|
const undoFile = () => {
|
|
829
|
+
if (batchBusyRef.current || !pendingRegions.length) return
|
|
830
|
+
batchBusyRef.current = true
|
|
824
831
|
actMany([...pendingRegions].reverse().map((r) => itemOf(r, true))).then(({ ok, fail }) => {
|
|
832
|
+
batchBusyRef.current = false
|
|
825
833
|
reloadFile(true)
|
|
826
834
|
emitRefresh()
|
|
827
835
|
setStatus('已不采纳 ' + ok + ' 处差异' + (fail ? ',' + fail + ' 处失败' : ''))
|
|
828
836
|
if (fail) setError(fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
|
|
829
|
-
})
|
|
837
|
+
}).catch(() => { batchBusyRef.current = false })
|
|
830
838
|
}
|
|
831
839
|
|
|
832
840
|
// 所有差异文件的待处理 hunk 列表(二级菜单 Keep All / Undo All 用)
|
package/src/rpc.ts
CHANGED
|
@@ -34,10 +34,75 @@ import { readDevForm, setDevForm } from './devForm.js'
|
|
|
34
34
|
import { normalizeRel, toTreeEntries } from './tree.js'
|
|
35
35
|
import { revealInExplorer } from './reveal.js'
|
|
36
36
|
|
|
37
|
-
/** cwd →
|
|
37
|
+
/** cwd → 内存缓冲:行数组 + 累计字节数 + 待触发 flush 定时器(攒批落盘,避免每条日志全文件读改写)。 */
|
|
38
|
+
const debugBuffers = new Map<string, { lines: string[]; len: number; timer: ReturnType<typeof setTimeout> | null }>()
|
|
39
|
+
/** cwd → Promise 链:串行化 debug 日志落盘(fs read+write 非原子,避免并发丢行)。 */
|
|
38
40
|
const debugWriteQueues = new Map<string, Promise<void>>()
|
|
39
41
|
/** debug 日志单文件上限:超限截断保留尾部,防文件无限增长拖慢每次追加。 */
|
|
40
42
|
const DEBUG_LOG_CAP = 512 * 1024
|
|
43
|
+
/** debug 日志批量缓冲上限:攒满即落盘(一次读改写),上限内不逐条写文件。 */
|
|
44
|
+
const DEBUG_BUF_CAP = 32 * 1024
|
|
45
|
+
/** debug 日志空闲 flush 延迟:缓冲未满时,静默一段时间后落盘一次。 */
|
|
46
|
+
const DEBUG_FLUSH_IDLE_MS = 1000
|
|
47
|
+
/** cwd → 上次 stale 自动清理时间:全量轮询(EditorView/DiffBadge/Dock 各自 5s)节流,避免每轮都读文件算指纹。 */
|
|
48
|
+
const staleCheckedAt = new Map<string, number>()
|
|
49
|
+
/** stale 自动清理最小间隔。 */
|
|
50
|
+
const STALE_CHECK_MIN_MS = 10_000
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 调试日志入队:先攒内存缓冲(行 + 字节数),满 DEBUG_BUF_CAP 立即落盘,
|
|
54
|
+
* 否则空闲 DEBUG_FLUSH_IDLE_MS 后落盘;落盘 = 读旧文件 + 追加整批 + 超限截断 + 一次写入。
|
|
55
|
+
* @author ddj 2026年08月26号
|
|
56
|
+
* @param ctx DSH 上下文
|
|
57
|
+
* @param cwd 工作区(日志文件按工作区旁车存放)
|
|
58
|
+
* @param policy 会话沙箱策略(入队时刻捕获)
|
|
59
|
+
* @param line 单条日志文本(不含换行)
|
|
60
|
+
*/
|
|
61
|
+
function enqueueDebug(ctx: Ctx, cwd: string, policy: unknown, line: string): void {
|
|
62
|
+
const st = debugBuffers.get(cwd) ?? { lines: [], len: 0, timer: null }
|
|
63
|
+
st.lines.push(line)
|
|
64
|
+
st.len += line.length
|
|
65
|
+
const flush = () => {
|
|
66
|
+
st.timer = null
|
|
67
|
+
const batch = st.lines
|
|
68
|
+
st.lines = []
|
|
69
|
+
st.len = 0
|
|
70
|
+
void flushDebug(ctx, cwd, policy, batch)
|
|
71
|
+
}
|
|
72
|
+
if (st.len >= DEBUG_BUF_CAP) {
|
|
73
|
+
if (st.timer) clearTimeout(st.timer)
|
|
74
|
+
flush()
|
|
75
|
+
} else if (!st.timer) {
|
|
76
|
+
st.timer = setTimeout(flush, DEBUG_FLUSH_IDLE_MS)
|
|
77
|
+
}
|
|
78
|
+
debugBuffers.set(cwd, st)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 批量落盘一条 debug 日志缓冲:读旧文件 → 追加 → 超上限截断保留尾部 → 一次写回。
|
|
83
|
+
* 串行链保证同一 cwd 的读改写不交错丢行;写失败静默忽略(调试日志不阻塞业务)。
|
|
84
|
+
* @author ddj 2026年08月26号
|
|
85
|
+
* @param ctx DSH 上下文
|
|
86
|
+
* @param cwd 工作区
|
|
87
|
+
* @param policy 会话沙箱策略(enqueue 时刻捕获)
|
|
88
|
+
* @param batch 本批日志行
|
|
89
|
+
*/
|
|
90
|
+
function flushDebug(ctx: Ctx, cwd: string, policy: unknown, batch: string[]): Promise<void> {
|
|
91
|
+
const prev = debugWriteQueues.get(cwd) ?? Promise.resolve()
|
|
92
|
+
const task = prev.then(async () => {
|
|
93
|
+
try {
|
|
94
|
+
const fs = ctx.get('fs')
|
|
95
|
+
if (!fs) return
|
|
96
|
+
const target = await fs.resolve('.dsh-edit-review-debug.log', { cwd })
|
|
97
|
+
const old = await fs.readText(target).catch(() => '')
|
|
98
|
+
const appended = old + batch.map((l) => new Date().toISOString() + ' ' + l + '\n').join('')
|
|
99
|
+
const next = appended.length > DEBUG_LOG_CAP ? appended.slice(-Math.floor(DEBUG_LOG_CAP / 2)) : appended
|
|
100
|
+
await fs.writeText(target, next, void 0, void 0, policy)
|
|
101
|
+
} catch (e) { /* 写日志失败忽略 */ }
|
|
102
|
+
})
|
|
103
|
+
debugWriteQueues.set(cwd, task)
|
|
104
|
+
return task
|
|
105
|
+
}
|
|
41
106
|
|
|
42
107
|
/** 记录 → 客户端视图(不含 before 全文,仅长度)。 */
|
|
43
108
|
function recView(record: DiffRecord): RecordView {
|
|
@@ -126,7 +191,16 @@ export function buildHandlers(ctx: Ctx, registry: Registry, searcher = newSearch
|
|
|
126
191
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
127
192
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
128
193
|
const want = Array.isArray(args.callIds) ? new Set(args.callIds) : null
|
|
129
|
-
|
|
194
|
+
// 全量轮询时自动清理 stale 幽灵差异(批量决策后的即时刷新可 skipStale 跳过);
|
|
195
|
+
// 三个组件各自 5s 全量 list,stale 检查节流到 STALE_CHECK_MIN_MS 一次,
|
|
196
|
+
// 避免每轮都对全部记录读文件 + 算指纹(keepall 期间多组件同时刷新的主要 host 开销)。
|
|
197
|
+
if (!want && args.skipStale !== true) {
|
|
198
|
+
const now = Date.now()
|
|
199
|
+
if (now - (staleCheckedAt.get(sc.cwd) ?? 0) > STALE_CHECK_MIN_MS) {
|
|
200
|
+
staleCheckedAt.set(sc.cwd, now)
|
|
201
|
+
await autoArchiveStale(ctx, sc.session, sc.cwd, bucket)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
130
204
|
const out: RecordView[] = []
|
|
131
205
|
for (const rec of bucket.values()) {
|
|
132
206
|
// 面板全量查询过滤已归档;聊天条按 callId 查询保留(状态徽章仍需正确显示)
|
|
@@ -293,26 +367,12 @@ export function buildHandlers(ctx: Ctx, registry: Registry, searcher = newSearch
|
|
|
293
367
|
return { ok: true, path: args.path, batch: batch ?? null }
|
|
294
368
|
},
|
|
295
369
|
'edrv.debug': async (args) => {
|
|
296
|
-
// 诊断日志:client 上报 →
|
|
370
|
+
// 诊断日志:client 上报 → 内存缓冲批量落盘 .dsh-edit-review-debug.log(console 不一定落盘,文件可靠)。
|
|
371
|
+
// 只出现在调试开关开启时(client dbg 默认关),终端仍逐条打印便于实时观察。
|
|
297
372
|
const sc = await requireSession(ctx, args.sessionId)
|
|
298
373
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
299
|
-
const fs = ctx.get('fs')
|
|
300
374
|
const text = String(args.text ?? '')
|
|
301
|
-
|
|
302
|
-
const task = prev.then(async () => {
|
|
303
|
-
try {
|
|
304
|
-
const target = await fs.resolve('.dsh-edit-review-debug.log', { cwd: sc.cwd })
|
|
305
|
-
const old = await fs.readText(target).catch(() => '')
|
|
306
|
-
const line = new Date().toISOString() + ' ' + text + '\n'
|
|
307
|
-
// 超上限时截断保留尾部:单文件体积有界,每次追加的读改写成本不随会话时长增长
|
|
308
|
-
const next = old.length + line.length > DEBUG_LOG_CAP
|
|
309
|
-
? old.slice(-Math.floor(DEBUG_LOG_CAP / 2)) + line
|
|
310
|
-
: old + line
|
|
311
|
-
await fs.writeText(target, next, void 0, void 0, policyOf(ctx, sc.session))
|
|
312
|
-
} catch (e) { /* 写日志失败忽略 */ }
|
|
313
|
-
})
|
|
314
|
-
debugWriteQueues.set(sc.cwd, task)
|
|
315
|
-
await task
|
|
375
|
+
enqueueDebug(ctx, sc.cwd, policyOf(ctx, sc.session), text)
|
|
316
376
|
console.error('[edrv-debug] ' + text)
|
|
317
377
|
return { ok: true }
|
|
318
378
|
},
|