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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 dsh-better-sidebar 侧边栏与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚),状态持久化到工作区旁车",
5
5
  "keywords": [
6
6
  "dsh",
@@ -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
- * 诊断日志:统一走 host edrv.debug 落盘,避免 console 刷屏。
20
- * @author ddj 2026年08月20号
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
- setRecords((prev) => Object.assign({}, prev, next))
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 → Promise 链:串行化 debug 日志追加(fs read+write 非原子,避免并发丢行)。 */
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
- if (!want && args.skipStale !== true) await autoArchiveStale(ctx, sc.session, sc.cwd, bucket) // 全量轮询时自动清理 stale 幽灵差异(批量决策后的即时刷新可 skipStale 跳过)
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 上报 → 写入工作区旁车 .dsh-edit-review-debug.log(console 不一定落盘,文件可靠)
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
- const prev = debugWriteQueues.get(sc.cwd) || Promise.resolve()
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
  },