dsh-vscode-mode 0.7.0 → 0.9.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 (60) hide show
  1. package/README.md +37 -6
  2. package/assets/icon.svg +7 -0
  3. package/cordis.patch.yml +16 -0
  4. package/lib/client.js +2804 -300
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +1975 -191
  7. package/lib/index.js.map +1 -1
  8. package/locale/en.json +4 -0
  9. package/locale/zh.json +4 -0
  10. package/package.json +4 -2
  11. package/src/ai/inline.ts +19 -2
  12. package/src/ai/svnTriage.ts +150 -0
  13. package/src/capture.ts +225 -51
  14. package/src/client/addToConversation.ts +203 -1
  15. package/src/client/compat.ts +134 -4
  16. package/src/client/imagePreview.ts +76 -2
  17. package/src/client/index.ts +65 -36
  18. package/src/client/monaco/lsp/lspClient.ts +46 -0
  19. package/src/client/monaco/lsp/providers.ts +245 -1
  20. package/src/client/nativeOpenStore.ts +91 -0
  21. package/src/client/officialSidebar.ts +11 -45
  22. package/src/client/outline/index.ts +1 -1
  23. package/src/client/pdf/pdfPanel.ts +18 -0
  24. package/src/client/pendingNav.ts +141 -0
  25. package/src/client/settingsContext.ts +6 -4
  26. package/src/client/sidebar/panels/FileExplorer.ts +2 -15
  27. package/src/client/sidebar/panels/SvnPanel.ts +682 -72
  28. package/src/client/sidebar/panels/index.ts +1 -1
  29. package/src/client/sidebar/panels/svnListBudget.ts +58 -0
  30. package/src/client/state/records.ts +7 -1
  31. package/src/client/styles/editor.css +47 -5
  32. package/src/client/svnStatus.ts +206 -1
  33. package/src/client/svnStore.ts +6 -1
  34. package/src/client/ui/AiSettings.ts +47 -14
  35. package/src/client/ui/DiffBox.ts +1 -1
  36. package/src/client/ui/DiffLauncher.ts +6 -4
  37. package/src/client/ui/EditorView.ts +149 -22
  38. package/src/client/ui/McpSettings.ts +30 -0
  39. package/src/client/ui/PerfSettings.ts +4 -1
  40. package/src/client/ui/SvnAiPlanDialog.ts +293 -0
  41. package/src/client/ui/icons.ts +91 -0
  42. package/src/compat.ts +26 -5
  43. package/src/dshVersion.ts +2 -1
  44. package/src/fileOpenSettings.ts +274 -49
  45. package/src/index.ts +18 -1
  46. package/src/lsp/client.ts +24 -0
  47. package/src/lsp/rpc.ts +57 -0
  48. package/src/lsp/server.ts +272 -2
  49. package/src/model.ts +2 -1
  50. package/src/perf.ts +90 -4
  51. package/src/remoteWorkspace.ts +177 -0
  52. package/src/routes.ts +13 -1
  53. package/src/rpc.ts +115 -39
  54. package/src/shared/ai.ts +12 -0
  55. package/src/shared/lsp.ts +79 -0
  56. package/src/shared/nativeOpen.ts +91 -0
  57. package/src/shared/rpc.ts +162 -2
  58. package/src/shared/svn.ts +296 -10
  59. package/src/shared/types.ts +5 -1
  60. package/src/svn.ts +235 -13
@@ -11,10 +11,13 @@ import React from 'react'
11
11
  import { dbg, rpc } from '../rpc.js'
12
12
  import { emitFileChanged, emitRefresh } from '../events.js'
13
13
  import { langOf, loadMonaco, snippetLanguageOf } from '../monaco/loader.js'
14
- import { dataUrlOf, isImagePath, isSvgPath } from '../imagePreview.js'
14
+ import { clampZoom, clearZoomMem, dataUrlOf, isImagePath, isSvgPath, recallZoom, rememberZoom, zoomKeyOf, zoomStepOf } from '../imagePreview.js'
15
15
  import { isMarkdownPath } from '../markdownPreview.js'
16
16
  import { MarkdownPanel } from '../md/mdPanel.js'
17
- import { base64ToBytes, isPdfPath } from '../pdfPreview.js'
17
+ import { base64ToBytes, bytesToBase64, isPdfPath } from '../pdfPreview.js'
18
+ import { inNativePath, tryNativeOpen } from '../nativeOpenStore.js'
19
+ import { clearPendingNav, putPendingNav, readPendingNav } from '../pendingNav.js'
20
+ import { readBinaryPreview } from '../../shared/rpc.js'
18
21
  import {
19
22
  clearBaseline,
20
23
  clearReadVersion,
@@ -121,6 +124,22 @@ function dirOfRel(relPath) {
121
124
  /** 跳转目标高亮的保留时长(LSP/搜索跳转落地后给用户的位置提示,到期自动清除)。 */
122
125
  const NAV_FLASH_MS = 1200
123
126
 
127
+ /** 二进制通道回退日志的一次性闸(整页仅报一次,避免每次读图/读 PDF 刷屏)。 */
128
+ let binFallbackLogged = false
129
+
130
+ /**
131
+ * 二进制通道回退的一次性诊断日志:readBinaryPreview 本次尝试过新通道并失败时上报。
132
+ * 有效期:随页面生命周期仅一条;dbg 本身受诊断开关控制(关=零输出零落盘)。
133
+ * @author ddj 2026年09月22号
134
+ * @param sid 会话 id
135
+ * @param out readBinaryPreview 统一结果(带 probed 才说明本次尝试过新通道)
136
+ */
137
+ const noteBinFallback = (sid, out) => {
138
+ if (binFallbackLogged || !out?.probed) return
139
+ binFallbackLogged = true
140
+ dbg(sid, 'edrv.readBinary 不可用(host 未接线/旧版或网络失败),已回退 base64', 'debug')
141
+ }
142
+
124
143
  /**
125
144
  * 从 Monaco 语言目录取可选语言 id 列表(代码片段新建时的语言下拉来源)。
126
145
  * 旧版 Monaco / 目录不可读时返回 undefined,由调用方回落 shared 的内置常量。
@@ -197,6 +216,8 @@ export function EditorView(props) {
197
216
  const [imageSrc, setImageSrc] = React.useState(null) // 图片预览 data URL(图片 tab 专用,文本 tab 恒为 null)
198
217
  const [imgSize, setImgSize] = React.useState(null) // 图片自然尺寸 { w, h }(路径栏 meta)
199
218
  const [imgBroken, setImgBroken] = React.useState(false) // 图片解码失败(onError),显示占位与重试
219
+ const [imgZoom, setImgZoom] = React.useState(null) // 图片缩放比例(null=适应宽度;会话+路径级记忆,见 imagePreview.zoomKeyOf)
220
+ const imgElRef = React.useRef(null) // 预览 <img> 元素(适应宽度态下步进取实测缩放基准)
200
221
  const svgTextRef = React.useRef(new Set()) // 强制以文本打开的 SVG 路径集合(toggleSvgText 维护)
201
222
  const mdPreviewRef = React.useRef(new Set()) // 处于预览态的 Markdown 路径集合(toggleMdPreview 维护;不持久化)
202
223
  const [pdfBytes, setPdfBytes] = React.useState(null) // 当前 PDF tab 的原始字节(null=加载中/非 PDF)
@@ -828,26 +849,36 @@ export function EditorView(props) {
828
849
  setImgSize(null)
829
850
  setImgBroken(false)
830
851
  setLoadStage({ progress: monaco ? 72 : 12, message: '读取图片…' })
831
- rpc('edrv.read', { sessionId: sid, path, encoding: 'base64' }).then((res) => {
852
+ readBinaryPreview(rpc, { sessionId: sid, path }).then((out) => {
832
853
  if (seq !== loadSeqRef.current || path !== active) return
833
- if (res && res.ok && res.encoding === 'base64') {
834
- if (!res.mime) {
854
+ if (out.ok && out.via === 'binary') {
855
+ // 新通道:原始字节本地组 data URL(线上省 base64,本地编码为 img src 所需不可省)
856
+ recordBaseline(scope, path, out.version || version)
857
+ clearReadVersion(scope, path) // 刚读过内容:已读版本台账失效,下一轮按新版本比对
858
+ setImageSrc(dataUrlOf(bytesToBase64(out.bytes), out.mime))
859
+ setLoadStage({ progress: 100, message: '图片已就绪' })
860
+ setStatus('已加载')
861
+ return
862
+ }
863
+ noteBinFallback(sid, out)
864
+ if (out.ok && out.via === 'base64') {
865
+ if (!out.mime) {
835
866
  setLoadError('host 版本过旧:图片响应缺少 mime')
836
867
  setError('host 版本过旧:图片响应缺少 mime')
837
868
  setStatus('读取失败')
838
869
  return
839
870
  }
840
- const imgVersion = res.version ?? version
871
+ const imgVersion = out.version ?? version
841
872
  recordBaseline(scope, path, imgVersion)
842
873
  clearReadVersion(scope, path) // 刚读过内容:已读版本台账失效,下一轮按新版本比对
843
- setImageSrc(dataUrlOf(res.content, res.mime))
874
+ setImageSrc(dataUrlOf(out.content, out.mime))
844
875
  setLoadStage({ progress: 100, message: '图片已就绪' })
845
876
  setStatus('已加载')
846
877
  return
847
878
  }
848
- const base = res?.error ? String(res.error) : '读取失败'
879
+ const base = out.error ? String(out.error) : '读取失败'
849
880
  // 带出 host 解析后的真实路径:跳转失败时一眼看出是路径解析错还是目标不存在
850
- const message = res?.resolvedPath ? base + ':' + String(res.resolvedPath) : base
881
+ const message = out.resolvedPath ? base + ':' + String(out.resolvedPath) : base
851
882
  setLoadError(message)
852
883
  setError(message)
853
884
  setStatus('读取失败')
@@ -884,21 +915,34 @@ export function EditorView(props) {
884
915
  setStatus('已加载')
885
916
  return
886
917
  }
887
- rpc('edrv.read', { sessionId: sid, path, encoding: 'base64' }).then((res) => {
918
+ readBinaryPreview(rpc, { sessionId: sid, path }).then((out) => {
888
919
  if (seq !== loadSeqRef.current || path !== active) return
889
- if (res && res.ok && res.encoding === 'base64') {
890
- const pdfVersion = res.version ?? version
920
+ if (out.ok && out.via === 'binary') {
921
+ const pdfVersion = out.version || version
891
922
  recordBaseline(scope, path, pdfVersion)
892
923
  clearReadVersion(scope, path) // 刚读过内容:已读版本台账失效,下一轮按新版本比对
893
- cache.set(path, res.content)
924
+ // 缓存仍存 base64(pdfB64CacheRef 形状不变,命中路径零改动);字节直通面板省一次解码
925
+ cache.set(path, bytesToBase64(out.bytes))
894
926
  while (cache.size > 6) cache.delete(cache.keys().next().value)
895
- setPdfBytes(base64ToBytes(res.content))
927
+ setPdfBytes(out.bytes)
896
928
  setLoadStage({ progress: 100, message: 'PDF 已就绪' })
897
929
  setStatus('已加载')
898
930
  return
899
931
  }
900
- const base = res?.error ? String(res.error) : '读取失败'
901
- const message = res?.resolvedPath ? base + ':' + String(res.resolvedPath) : base
932
+ noteBinFallback(sid, out)
933
+ if (out.ok && out.via === 'base64') {
934
+ const pdfVersion = out.version ?? version
935
+ recordBaseline(scope, path, pdfVersion)
936
+ clearReadVersion(scope, path) // 刚读过内容:已读版本台账失效,下一轮按新版本比对
937
+ cache.set(path, out.content)
938
+ while (cache.size > 6) cache.delete(cache.keys().next().value)
939
+ setPdfBytes(base64ToBytes(out.content))
940
+ setLoadStage({ progress: 100, message: 'PDF 已就绪' })
941
+ setStatus('已加载')
942
+ return
943
+ }
944
+ const base = out.error ? String(out.error) : '读取失败'
945
+ const message = out.resolvedPath ? base + ':' + String(out.resolvedPath) : base
902
946
  setLoadError(message)
903
947
  setError(message)
904
948
  setStatus('读取失败')
@@ -1369,6 +1413,10 @@ export function EditorView(props) {
1369
1413
  if (!event?.detail?.scope || event.detail.scope === scope) setSvnStatus(getSvnStatus(scope))
1370
1414
  }
1371
1415
  window.addEventListener('edrv:svn-status', onSvnStatus)
1416
+ // 缓存命中时 ensure 直接 return、不广播事件,须同步读回当前 scope 的载荷:
1417
+ // 否则从非 SVN 工作区切回 SVN 工作区后,状态残留上一个工作区的 managed:false,
1418
+ // 右键菜单/页签菜单的 SVN 组会整组消失(与下方变更清单 effect 的同步语句同理)。
1419
+ setSvnStatus(getSvnStatus(scope))
1372
1420
  return () => window.removeEventListener('edrv:svn-status', onSvnStatus)
1373
1421
  }, [sessionId, scope])
1374
1422
 
@@ -1908,6 +1956,7 @@ export function EditorView(props) {
1908
1956
  // model 不在此销毁:跨挂载缓存按作用域存活(modelCache 切作用域时统一释放)
1909
1957
  for (const ctl of pdfCtlRef.current.values()) ctl.destroy()
1910
1958
  pdfCtlRef.current.clear()
1959
+ clearZoomMem() // 图片缩放记忆随 EditorView 卸载清空(会话级生命周期,不持久化)
1911
1960
  const root = editorRef.current && editorRef.current.getDomNode ? editorRef.current.getDomNode() : null
1912
1961
  if (root) {
1913
1962
  const ov = root.querySelector('.edrv-minus-overlay')
@@ -2209,6 +2258,12 @@ export function EditorView(props) {
2209
2258
  // 「已就绪」会提前定位;随后真实内容到达触发 model 重设/内容写入,Monaco 会重置光标,
2210
2259
  // 落点丢失且 pendingFocus 已消费,最终停在 {1,1}。实测(v0.4.6,Ctrl+点击枚举成员):
2211
2260
  // opener 正确派发 line=349,落地后编辑器实际停在 1:1 且无高亮;同文件跳转不换内容故不复现。
2261
+ //
2262
+ // ⚠️ 第三次事故(2026-09-23 实测取证):并行改码触发插件 client HMR 热重载
2263
+ // (保留 console 装配日志重复 6-7 轮为证)→ 本组件被重挂载 → 挂载级 pendingFocusRef
2264
+ // 随旧实例销毁 → 「首开只打开文件不落行、热重载平息后第二次才正常」。
2265
+ // 修复:openFileAt 同步写 window.__edrvPendingNav__ 槽(跨热重载存活),本 effect 取
2266
+ // ref || 槽、成功落点后一并清;并加模型身份守卫(模型不是目标文件时不清、等重跑)。
2212
2267
  React.useEffect(() => {
2213
2268
  const ed = editorRef.current
2214
2269
  if (!ed || !contentReady) return
@@ -2226,11 +2281,24 @@ export function EditorView(props) {
2226
2281
  ed.focus()
2227
2282
  return
2228
2283
  }
2229
- const pf = pendingFocusRef.current
2284
+ // 快路径 = 挂载级 ref;交接路径 = window 槽(热重载重挂载后 ref 已丢、槽仍在)
2285
+ const refPf = pendingFocusRef.current
2286
+ const slotNav = refPf ? null : readPendingNav()
2287
+ const pf = refPf || (slotNav
2288
+ ? { path: slotNav.path, region: null, line: slotNav.line ?? null, column: slotNav.column ?? 1, endLine: slotNav.endLine ?? null, endColumn: slotNav.endColumn ?? null }
2289
+ : null)
2230
2290
  if (!pf || !sameFile(pf.path, active)) return
2291
+ // 单一清除点:ref 与 window 槽一并处理,防残留二次跳
2292
+ const dropPending = () => {
2293
+ pendingFocusRef.current = null
2294
+ if (pf.line != null) clearPendingNav()
2295
+ }
2296
+ // 模型身份守卫:编辑器模型必须已是目标文件才落点;不匹配则保留 pending(不清),
2297
+ // 依赖变化后的下一次运行再落(model 同步 effect 声明在本 effect 之前、同帧先执行)。
2231
2298
  if (pf.line != null) {
2299
+ if (!sameFile(modelPathOf(ed), pf.path)) return
2232
2300
  // 搜索命中 / LSP 跳转:直接定位到行/列(不依赖差异区域)并高亮目标区域
2233
- pendingFocusRef.current = null
2301
+ dropPending()
2234
2302
  const line = Math.max(1, pf.line)
2235
2303
  const column = Math.max(1, pf.column ?? 1)
2236
2304
  ed.revealLineInCenter(line)
@@ -2241,7 +2309,8 @@ export function EditorView(props) {
2241
2309
  }
2242
2310
  const target = pf.region || pendingRegions[0]
2243
2311
  if (!target) return
2244
- pendingFocusRef.current = null
2312
+ if (!sameFile(modelPathOf(ed), pf.path)) return
2313
+ dropPending()
2245
2314
  ed.revealLineInCenter(Math.max(1, target.start ?? 1))
2246
2315
  ed.setPosition({ lineNumber: Math.max(1, target.start ?? 1), column: 1 })
2247
2316
  ed.focus()
@@ -2401,15 +2470,58 @@ export function EditorView(props) {
2401
2470
  }
2402
2471
  toggleMdPreviewRef.current = toggleMdPreview
2403
2472
 
2473
+ /** 当前图片的会话+路径级缩放记忆键(页签切换回来保留同一缩放)。 */
2474
+ const imgZoomKey = zoomKeyOf(sessionId, active)
2475
+
2476
+ // 页签/会话切换时恢复该图片的记忆缩放(无记忆 = 初始适应宽度,等价 PDF page-width)
2477
+ React.useEffect(() => { setImgZoom(recallZoom(imgZoomKey)) }, [imgZoomKey])
2478
+
2404
2479
  /**
2405
- * 图片预览面板:工具条(尺寸 meta / SVG 文本切换 / 刷新)+ 棋盘底自适应图片。
2406
- * @author ddj 2026年09月08号
2480
+ * 应用图片缩放并写入页签记忆(null = 适应宽度;数值经 clampZoom 收敛 25%–400%)。
2481
+ * @author ddj 2026年09月22号
2482
+ * @param next 目标缩放(null = 适应宽度)
2483
+ */
2484
+ const applyImgZoom = (next) => {
2485
+ const value = next === null ? null : clampZoom(next)
2486
+ setImgZoom(value)
2487
+ rememberZoom(imgZoomKey, value)
2488
+ }
2489
+
2490
+ /**
2491
+ * 图片缩放步进(10%/档,clamp 25%–400%);适应宽度态先按渲染尺寸实测当前缩放作基准。
2492
+ * @author ddj 2026年09月22号
2493
+ * @param dir 方向:1 放大 / -1 缩小
2494
+ */
2495
+ const stepImgZoom = (dir) => {
2496
+ const el = imgElRef.current
2497
+ const measured = el && el.naturalWidth ? el.clientWidth / el.naturalWidth : 1
2498
+ applyImgZoom(zoomStepOf(imgZoom ?? measured, dir))
2499
+ }
2500
+
2501
+ /**
2502
+ * 图片预览面板:工具条(尺寸 meta / 缩放 −+适应宽度 / SVG 文本切换 / 刷新)+ 棋盘底图片。
2503
+ * 初始适应宽度(等价 PDF page-width,0.1.7 官方统一缩放);按钮样式与 PDF 工具条同款。
2504
+ * @author ddj 2026年09月08号 / 2026年09月22号
2407
2505
  */
2408
2506
  const imagePanel = () => React.createElement('div', { className: 'edrv-imgview' },
2409
2507
  React.createElement('div', { className: 'edrv-imgview-bar' },
2410
2508
  React.createElement('span', { className: 'edrv-imgview-meta' },
2411
2509
  imgBroken ? '图片无法显示' : (imgSize ? imgSize.w + '×' + imgSize.h : '…')),
2412
2510
  React.createElement('span', { style: { flex: 1 } }),
2511
+ React.createElement('span', { className: 'edrv-pdf-page' },
2512
+ imgZoom === null ? '适应' : Math.round(imgZoom * 100) + '%'),
2513
+ React.createElement('button', {
2514
+ className: 'edrv-pill edrv-pill-ghost edrv-pdf-zoom', title: '缩小(10%/档)',
2515
+ onClick: () => stepImgZoom(-1),
2516
+ }, '−'),
2517
+ React.createElement('button', {
2518
+ className: 'edrv-pill edrv-pill-ghost edrv-pdf-zoom', title: '放大(10%/档)',
2519
+ onClick: () => stepImgZoom(1),
2520
+ }, '+'),
2521
+ React.createElement('button', {
2522
+ className: 'edrv-pill edrv-pill-ghost', title: '恢复适应宽度',
2523
+ disabled: imgZoom === null, onClick: () => applyImgZoom(null),
2524
+ }, '适应宽度'),
2413
2525
  (isSvgPath(active) ? React.createElement('button', {
2414
2526
  className: 'edrv-pill edrv-pill-ghost', title: '以文本方式查看/编辑(SVG 为文本格式)',
2415
2527
  onClick: () => toggleSvgText(active),
@@ -2422,8 +2534,12 @@ export function EditorView(props) {
2422
2534
  React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', onClick: () => reloadFile() }, '重试'))
2423
2535
  : React.createElement('img', {
2424
2536
  className: 'edrv-imgview-img',
2537
+ ref: imgElRef,
2425
2538
  src: imageSrc,
2426
2539
  alt: active || '',
2540
+ style: (imgZoom !== null && imgSize)
2541
+ ? { width: Math.round(imgSize.w * imgZoom) + 'px', maxWidth: 'none', maxHeight: 'none' }
2542
+ : undefined,
2427
2543
  onLoad: (e) => { setImgBroken(false); setImgSize({ w: e.target.naturalWidth, h: e.target.naturalHeight }) },
2428
2544
  onError: () => setImgBroken(true),
2429
2545
  }))))
@@ -2996,6 +3112,10 @@ export function EditorView(props) {
2996
3112
 
2997
3113
  const openFile = (path, focusDiff) => {
2998
3114
  if (!path) return
3115
+ // 原生打开范围(nativeOpenExts 设置):命中且非差异跳转 → 走官方渲染器
3116
+ //(focusDiff=true 跳过:差异审查对象必为本插件编辑的文本文件,审查优先);
3117
+ // handler 缺失/失败(旧版无 openResource / seat 未挂)→ 回退现状页签打开,不阻断
3118
+ if (!focusDiff && inNativePath(path) && tryNativeOpen(path)) return
2999
3119
  // 打开文件即离开基线差异审阅态(差异视图是「当前文件」的临时视图)
3000
3120
  setSvnDiff(null)
3001
3121
  recordNav()
@@ -3023,6 +3143,9 @@ export function EditorView(props) {
3023
3143
  const normalized = addTabNorm(path, true)
3024
3144
  if (!normalized) return
3025
3145
  pendingFocusRef.current = { path: normalized, region: null, line: line ?? null, column: column ?? 1, endLine: endLine ?? null, endColumn: endColumn ?? null }
3146
+ // 第三次事故(2026-09-23):插件 HMR 热重载会重挂载本组件、挂载级 ref 随旧实例销毁 →
3147
+ // 落点意图丢失。同步落 window 槽(跨热重载存活)做交接,落点成功后与 ref 一并清。
3148
+ putPendingNav({ path: normalized, line: line ?? null, column: column ?? 1, endLine: endLine ?? null, endColumn: endColumn ?? null })
3026
3149
  setFocusRequest((value) => value + 1)
3027
3150
  }
3028
3151
 
@@ -3426,6 +3549,10 @@ export function EditorView(props) {
3426
3549
  : null
3427
3550
 
3428
3551
  const sidebarPanels = props.sidebarPanels
3552
+ // SVN 变更查表(文件树徽标 O(1) 查表用):O(n) 建表只随数据/作用域变化重算,
3553
+ // 不再随编辑区每轮渲染重建(条目多时每帧新建几千键的 Record 徒增 GC 压力)。
3554
+ // @author ddj 2026年09月23号
3555
+ const svnChangeMapMemo = React.useMemo(() => svnChangeMapOf(scope), [scope, svnChanges])
3429
3556
  const sidebarCtx = {
3430
3557
  sessionId,
3431
3558
  // 当前会话工作区(反应式 cwd;规则面板项目 Tab 自动匹配)
@@ -3445,7 +3572,7 @@ export function EditorView(props) {
3445
3572
  svn: svnStatus,
3446
3573
  // SVN 变更:面板列表 + 文件树徽标查表(客户端合成,不改 listDir 契约)
3447
3574
  svnChanges,
3448
- svnChangeMap: svnChangeMapOf(scope),
3575
+ svnChangeMap: svnChangeMapMemo,
3449
3576
  openSvnDiff: (p) => runSvnDiffBase(p),
3450
3577
  openSvnLocalPair: runSvnDiffLocalPair,
3451
3578
  refreshSvnChanges: () => refreshSvnChanges(sessionId, scope),
@@ -13,6 +13,7 @@ import { SettingsContext } from '../settingsContext.js'
13
13
  import { normalizeSidebarMinWidth, SIDEBAR_MIN_DEFAULT } from '../sidebarMin.js'
14
14
  import { EDITOR_LIMIT_CEIL, EDITOR_LIMIT_DEFAULT, normalizeMaxOpenEditors } from '../../shared/editorLimit.js'
15
15
  import { TORTOISE_DIR_DEFAULT } from '../../shared/svn.js'
16
+ import { DEFAULT_NATIVE_CSV } from '../../shared/nativeOpen.js'
16
17
  import { KeybindingsPanel } from './KeybindingsPanel.js'
17
18
  import { LspSettings } from './LspSettings.js'
18
19
  import { PerfSettings } from './PerfSettings.js'
@@ -139,6 +140,8 @@ function GeneralSettings({ registry }) {
139
140
  const [minDraft, setMinDraft] = React.useState(null) // 输入草稿(null=跟随已保存值;blur/Enter 提交)
140
141
  const [limit, setLimit] = React.useState(EDITOR_LIMIT_DEFAULT)
141
142
  const [limitDraft, setLimitDraft] = React.useState(null) // 页签上限草稿(同上提交语义)
143
+ const [nativeSaved, setNativeSaved] = React.useState(DEFAULT_NATIVE_CSV) // 原生打开范围已保存值
144
+ const [nativeDraft, setNativeDraft] = React.useState(null) // 原生打开范围草稿(同上提交语义)
142
145
  const settings = React.useContext(SettingsContext)
143
146
  const snapshot = settings?.getSnapshot?.()
144
147
  const loading = !snapshot || snapshot.status === 'loading'
@@ -151,6 +154,8 @@ function GeneralSettings({ registry }) {
151
154
  if (typeof next === 'string') setTool(next)
152
155
  setMinW(normalizeSidebarMinWidth(snap?.value?.sidebarMinWidth))
153
156
  setLimit(normalizeMaxOpenEditors(snap?.value?.maxOpenEditors))
157
+ const native = snap?.value?.nativeOpenExts
158
+ setNativeSaved(typeof native === 'string' && native.trim() ? native : DEFAULT_NATIVE_CSV)
154
159
  }
155
160
  onChange()
156
161
  return settings?.subscribe?.(onChange)
@@ -191,6 +196,18 @@ function GeneralSettings({ registry }) {
191
196
  setLimitDraft(null)
192
197
  saveLimit(limitDraft)
193
198
  }
199
+ /** 提交原生打开范围(逗号分隔后缀;空串回落默认让位清单后持久化)。 */
200
+ const saveNative = (raw) => {
201
+ const next = String(raw ?? '').trim() || DEFAULT_NATIVE_CSV
202
+ setNativeSaved(next); setNativeDraft(null); setBusy(true); setError('')
203
+ if (!settings?.set) { setError('设置服务不可用'); setBusy(false); return }
204
+ settings.set('nativeOpenExts', next).catch((e) => setError(String(e))).finally(() => setBusy(false))
205
+ }
206
+ /** 结束输入(blur/Enter)时提交原生打开范围草稿。 */
207
+ const commitNative = () => {
208
+ if (nativeDraft === null) return
209
+ saveNative(nativeDraft)
210
+ }
194
211
  const closeDevForm = () => {
195
212
  if (!window.confirm('关闭开发形态:插件将切换为正式版安装(版本依赖 + 删除工作区链接),pnpm 装配后需重启 DSH 生效。确认关闭?')) return
196
213
  setDevBusy(true)
@@ -249,6 +266,19 @@ function GeneralSettings({ registry }) {
249
266
  onKeyDown: (event) => { if (event.key === 'Enter') commitLimit() },
250
267
  }),
251
268
  React.createElement('small', null, '0 = 不限制;超限时关闭最久未使用的页签(固定页签除外)')),
269
+ React.createElement('label', { className: 'vsm-general-row' },
270
+ React.createElement('span', null, '原生打开范围'),
271
+ React.createElement('input', {
272
+ type: 'text',
273
+ value: nativeDraft ?? nativeSaved,
274
+ disabled: loading || unavailable || notReady || busy || snapshot?.writable === false,
275
+ title: '逗号分隔后缀(如 doc,xlsx):文件树点击命中时用 DSH 官方预览原生打开;留空 = 默认让位清单(Office + 不可预览二进制)',
276
+ placeholder: 'doc,xlsx,…(留空恢复默认)',
277
+ onChange: (event) => setNativeDraft(event.target.value),
278
+ onBlur: commitNative,
279
+ onKeyDown: (event) => { if (event.key === 'Enter') commitNative() },
280
+ }),
281
+ React.createElement('small', null, '逗号分隔;命中的文件用 DSH 原生预览打开,留空恢复默认(Office + 二进制,csv/tsv 需显式加入)')),
252
282
  ),
253
283
  ),
254
284
  React.createElement(SvnSettingsSection, null),
@@ -1,7 +1,7 @@
1
1
  // @ts-nocheck
2
2
  /**
3
3
  * dsh-vscode-mode client — PerfSettings:「性能优化」设置子页(会话卫生 + 压缩调优)。
4
- * - 会话盘点:全工作区/每会话体积、活跃标记,按体积排序;
4
+ * - 会话盘点:全工作区/每会话体积、活跃标记、官方归档/置顶徽标,按体积排序;
5
5
  * - 移出到归档(先规划→确认→执行→可恢复)与归档清除;
6
6
  * - DSH 内置压缩调优:写 profile patch(compaction-basic / tool-result-pruner 更低阈值,
7
7
  * 带备份与撤销,需重启生效);
@@ -67,6 +67,9 @@ function SessionRow({ row, checked, disabled, onToggle }) {
67
67
  React.createElement('span', { className: 'vsm-perf-cell vsm-perf-size ' + (row.bytes >= 1024 * 1024 ? 'big' : '') }, formatBytes(row.bytes)),
68
68
  React.createElement('span', { className: 'vsm-perf-cell vsm-perf-mtime', title: new Date(row.mtime).toLocaleString() }, when),
69
69
  row.active ? React.createElement('span', { className: 'vsm-perf-cell vsm-perf-active' }, '活跃') : null,
70
+ // 官方侧栏归档/置顶状态徽标(旧 host 不回传字段 → undefined 按 false,不渲染;样式复用面板现有 vsm-perf-active 徽标)
71
+ row.archived ? React.createElement('span', { className: 'vsm-perf-cell vsm-perf-active', title: '官方侧栏已归档(持久标志位,未搬目录)' }, '已归档') : null,
72
+ row.pinned ? React.createElement('span', { className: 'vsm-perf-cell vsm-perf-active', title: '官方侧栏已置顶' }, '已置顶') : null,
70
73
  )
71
74
  }
72
75