dsh-vscode-mode 0.4.2 → 0.4.4

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.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
5
5
  "keywords": [
6
6
  "dsh",
@@ -38,6 +38,18 @@ export function editorDockMode(activePath: string | null | undefined): 'editor'
38
38
  return activePath ? 'editor' : 'editor-empty'
39
39
  }
40
40
 
41
+ /**
42
+ * 文件是否仍可执行 Keep/Undo:可定位差异与无法定位的冲突差异都算待处理。
43
+ * 冲突差异的 newText 已不在文件中(被后续修改覆盖),由 host 按"已不存在"记录决策。
44
+ * @author ddj 2026年09月15号
45
+ * @param pendingCount 可定位的待处理差异数
46
+ * @param staleCount 无法定位(冲突)的待处理差异数
47
+ * @returns 两者任一大于 0 时为 true
48
+ */
49
+ export function canDecideFile(pendingCount: number, staleCount: number): boolean {
50
+ return Math.max(0, pendingCount) + Math.max(0, staleCount) > 0
51
+ }
52
+
41
53
  /**
42
54
  * 获取切换文件期间稳定展示的文件内差异数量。
43
55
  * @author ddj 2026年08月26号
@@ -54,6 +54,17 @@ export function emitRefresh(): void {
54
54
  window.dispatchEvent(new CustomEvent('edrv:refresh'))
55
55
  }
56
56
 
57
+ /**
58
+ * 磁盘文件变化(外部写入/删除):文件树按路径失效对应目录并强制重列。
59
+ * 与 edrv:refresh 分开:这里不触发差异记录重算与 stale 清理,只动目录缓存。
60
+ * @author ddj 2026年09月15号
61
+ * @param path 发生变化的文件路径
62
+ */
63
+ export function emitFileChanged(path: string): void {
64
+ if (!path) return
65
+ window.dispatchEvent(new CustomEvent('edrv:file-changed', { detail: { path } }))
66
+ }
67
+
57
68
  /** 打开指定路径到编辑区页签。 */
58
69
  export function emitOpenEditor(path: string): void {
59
70
  window.dispatchEvent(new CustomEvent('edrv:open-editor', { detail: { path } }))
@@ -28,6 +28,8 @@ const PREFETCH_EXCLUDED = new Set(['node_modules', '.git', '.hg', '.svn', '.pnpm
28
28
  const REVEAL_HIGHLIGHT_MS = 2000
29
29
  const REVEAL_RETRY_MAX = 6
30
30
  const REVEAL_RETRY_MS = 120
31
+ /** 外部文件变化重列去抖:一轮外部批量写入(如 agent 连写多文件)合并为一次重列。 */
32
+ const FILE_CHANGE_DEBOUNCE_MS = 400
31
33
 
32
34
  // --region 行图标(官方原语:目录文件夹图标 + 文件类型图标;缺失时回落纯文本)
33
35
 
@@ -125,6 +127,11 @@ export function FileExplorer(props) {
125
127
  const revealTryRef = React.useRef(0) // 当前定位的重试计数(行渲染需等目录加载)
126
128
  const revealInTreeRef = React.useRef(null) // 定位动作最新闭包(窗口监听读取)
127
129
  const treeRef = React.useRef(null) // 目录树容器(定位时按 data-edrv-path 查行)
130
+ const reloadDirRef = React.useRef(null) // loadDir 最新闭包(文件变化监听读取,防陈旧闭包)
131
+ const changedTimerRef = React.useRef(null) // 文件变化合并去抖计时器
132
+ const changedRelRef = React.useRef(new Set()) // 待重列的相对目录集合(去抖窗口内合并)
133
+ const dirsMapRef = React.useRef(null) // loadDir 最新闭包(文件变化监听读取,防陈旧闭包)
134
+ dirsMapRef.current = loadDir
128
135
 
129
136
  /** 渲染取数:内存态 → 本地条目缓存 → null(显示加载态)。 */
130
137
  const entriesOf = (rel) => dirsRef.current[rel] ?? entriesCacheGet(scope, rel) ?? null
@@ -261,6 +268,46 @@ export function FileExplorer(props) {
261
268
  void loadDir('', { force: true, prefetch: true })
262
269
  }
263
270
  refreshRef.current = refresh
271
+ reloadDirRef.current = loadDir
272
+
273
+ /**
274
+ * 磁盘文件变化(外部写入/删除/改名):把变化路径的父目录并入待重列集合,
275
+ * 去抖合并后对「已展开且存在」的目录强制重列(force 跳过 host 索引命中)。
276
+ * host 侧 fileVersions 已顺手失效目录树缓存,这里补上「已渲染行」的即时刷新。
277
+ * @author ddj 2026年09月15号
278
+ * @param path 发生变化的文件路径(工作区相对;绝对路径按最长已展开祖先匹配)
279
+ */
280
+ const onFileChanged = (path) => {
281
+ if (typeof path !== 'string' || !path) return
282
+ const rel = path.replace(/\\/g, '/').replace(/^\.\//, '')
283
+ const parts = rel.split('/').filter(Boolean)
284
+ if (parts.length > 1) changedRelRef.current.add(parts.slice(0, -1).join('/'))
285
+ // 绝对路径(或已在更深层目录):补上所有已展开的祖先目录
286
+ for (const dir of ancestorDirsOf(rel)) {
287
+ if (expandedRef.current[dir] === true) changedRelRef.current.add(dir)
288
+ }
289
+ if (changedTimerRef.current) return
290
+ changedTimerRef.current = window.setTimeout(() => {
291
+ changedTimerRef.current = null
292
+ const targets = [...changedRelRef.current]
293
+ changedRelRef.current.clear()
294
+ for (const dir of targets) {
295
+ if (dir !== '' && expandedRef.current[dir] !== true) continue
296
+ void reloadDirRef.current?.(dir, { force: true, prefetch: false })
297
+ }
298
+ }, FILE_CHANGE_DEBOUNCE_MS)
299
+ }
300
+
301
+ // edrv:file-changed(外部改动同步):按变化路径强制重列对应目录(去抖合并)
302
+ React.useEffect(() => {
303
+ const handler = (event) => onFileChanged(event?.detail?.path)
304
+ window.addEventListener('edrv:file-changed', handler)
305
+ return () => {
306
+ window.removeEventListener('edrv:file-changed', handler)
307
+ if (changedTimerRef.current) { clearTimeout(changedTimerRef.current); changedTimerRef.current = null }
308
+ }
309
+ // eslint-disable-next-line react-hooks/exhaustive-deps
310
+ }, [])
264
311
 
265
312
  React.useEffect(() => {
266
313
  tokensRef.current = {}
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import React from 'react'
8
8
  import { callIdAttr } from '../state/records.js'
9
- import { diffDockText } from '../diffDock.js'
9
+ import { canDecideFile, diffDockText } from '../diffDock.js'
10
10
  import { badgeOf } from './shared.js'
11
11
 
12
12
  /**
@@ -48,6 +48,8 @@ export function DiffBox(props) {
48
48
  const [detailsOpen, setDetailsOpen] = React.useState(false)
49
49
  const detailsId = React.useId()
50
50
  const canAct = pendingRegions.length > 0
51
+ // Keep/Undo 覆盖无法定位的冲突差异:它们仍占待处理列表,但不在 pendingRegions 内
52
+ const canDecide = canDecideFile(pendingRegions.length, staleRegions.length)
51
53
 
52
54
  const base = String(activePath || '').split(/[\\/]/).pop() || ''
53
55
 
@@ -114,8 +116,8 @@ export function DiffBox(props) {
114
116
  React.createElement('span', { className: 'edrv-diffbar-file edrv-diffbar-summary', title: activePath || '' }, base)),
115
117
 
116
118
  React.createElement('div', { className: 'edrv-diffbar-actions' },
117
- React.createElement('button', { className: 'edrv-pill edrv-pill-keep', title: '采纳当前文件的全部差异', disabled: !canAct, onClick: onAcceptFile }, '✓ Keep'),
118
- React.createElement('button', { className: 'edrv-pill edrv-pill-undo', title: '不采纳当前文件的全部差异(回滚)', disabled: !canAct, onClick: onUndoFile }, '↩ Undo'),
119
+ React.createElement('button', { className: 'edrv-pill edrv-pill-keep', title: '采纳当前文件的全部差异', disabled: !canDecide, onClick: onAcceptFile }, '✓ Keep'),
120
+ React.createElement('button', { className: 'edrv-pill edrv-pill-undo', title: '不采纳当前文件的全部差异(回滚)', disabled: !canDecide, onClick: onUndoFile }, '↩ Undo'),
119
121
  React.createElement('button', {
120
122
  className: 'edrv-pill edrv-pill-ghost edrv-diff-details-toggle',
121
123
  title: detailsOpen ? '收起差异操作' : '展开差异操作',