dsh-vscode-mode 0.12.0 → 0.13.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 (71) hide show
  1. package/README.md +129 -103
  2. package/assets/shell/dsh-open.cs +65 -149
  3. package/assets/shell/dsh-open.mjs +241 -0
  4. package/assets/shell/dsh-open.ps1 +28 -47
  5. package/assets/shell/dsh-open.sh +25 -66
  6. package/lib/client.js +12465 -11526
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +3889 -2336
  9. package/lib/index.js.map +1 -1
  10. package/package.json +9 -8
  11. package/skills/dsh-vscodemode-mcp/SKILL.md +94 -132
  12. package/src/childEnv.ts +19 -0
  13. package/src/client/addToConversation.ts +4 -2
  14. package/src/client/ai/inlineProvider.ts +1 -8
  15. package/src/client/commandBridge.ts +6 -2
  16. package/src/client/externalOpen.ts +40 -19
  17. package/src/client/index.ts +10 -4
  18. package/src/client/keybindings.ts +18 -1
  19. package/src/client/monaco/loader.ts +3 -1
  20. package/src/client/openFlow.ts +36 -28
  21. package/src/client/openReceipt.ts +64 -0
  22. package/src/client/panelCommands.ts +101 -0
  23. package/src/client/pdf/pdfLoader.ts +4 -2
  24. package/src/client/receiptState.ts +120 -0
  25. package/src/client/rpc.ts +30 -4
  26. package/src/client/shortcutRecorder.ts +161 -0
  27. package/src/client/shortcutsOfficial.ts +13 -4
  28. package/src/client/sidebarBridge.ts +7 -0
  29. package/src/client/ui/DiffBadge.ts +2 -1
  30. package/src/client/ui/EditorView.ts +237 -55
  31. package/src/client/ui/McpSettings.ts +6 -3
  32. package/src/client/ui/OfficialSideTab.ts +5 -2
  33. package/src/client/ui/QuickOpen.ts +10 -9
  34. package/src/client/ui/ShortcutSettings.ts +34 -54
  35. package/src/client/ui/commandCatalog.ts +5 -4
  36. package/src/client/useOpenReceipt.ts +128 -0
  37. package/src/compat.ts +5 -3
  38. package/src/dap/manager.ts +46 -13
  39. package/src/devForm.ts +252 -92
  40. package/src/externalHandoff.ts +93 -52
  41. package/src/fileOpenSettings.ts +1 -24
  42. package/src/hostImport.ts +19 -0
  43. package/src/httpGuard.ts +101 -0
  44. package/src/index.ts +106 -97
  45. package/src/integrate.ts +56 -50
  46. package/src/lsp/transport.ts +84 -127
  47. package/src/mcp.ts +58 -9
  48. package/src/mcpIsolation.ts +211 -102
  49. package/src/mcpProject.ts +220 -133
  50. package/src/mcpRuntime.ts +343 -0
  51. package/src/openBridge.ts +69 -0
  52. package/src/openInbox.ts +207 -0
  53. package/src/paths.ts +8 -5
  54. package/src/processTree.ts +63 -0
  55. package/src/reveal.ts +5 -3
  56. package/src/routes.ts +156 -154
  57. package/src/rpc.ts +14 -13
  58. package/src/shared/appUrl.ts +19 -0
  59. package/src/shared/externalOpen.ts +71 -0
  60. package/src/shared/integration.ts +2 -0
  61. package/src/shared/keybindings.ts +8 -5
  62. package/src/shared/mcp.ts +4 -1
  63. package/src/shared/rpc.ts +7 -4
  64. package/src/shared/svn.ts +6 -8
  65. package/src/svnPlanTool.ts +51 -0
  66. package/src/unityBridge.ts +26 -3
  67. package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +99 -203
  68. package/unity/com.dsh.editor/Editor/DshOpenWorker.cs +166 -0
  69. package/unity/com.dsh.editor/Editor/DshOpenWorker.cs.meta +2 -0
  70. package/unity/com.dsh.editor/README.md +16 -25
  71. package/unity/com.dsh.editor/package.json +2 -2
@@ -8,6 +8,9 @@
8
8
  * 作者 ddj 2026-08-20
9
9
  */
10
10
  import React from 'react'
11
+ import { useOpenReceipt } from '../useOpenReceipt.js'
12
+ import { acceptsLoad, receiptErrorFor, receiptPath, receiptReadyFor, receiptTabPath } from '../receiptState.js'
13
+ import { bindPanelCommand } from '../panelCommands.js'
11
14
  import { dbg, rpc } from '../rpc.js'
12
15
  import { emitFileChanged, emitRefresh } from '../events.js'
13
16
  import { langOf, loadMonaco, snippetLanguageOf } from '../monaco/loader.js'
@@ -235,6 +238,7 @@ export function EditorView(props) {
235
238
  const [dirtyMap, setDirtyMap] = React.useState({})
236
239
  const [content, setContent] = React.useState(null)
237
240
  const [contentPath, setContentPath] = React.useState(null)
241
+ const [receiptLoad, setReceiptLoad] = React.useState(null) // { sessionId, path, sequence, status, error }
238
242
  const [imageSrc, setImageSrc] = React.useState(null) // 图片预览 data URL(图片 tab 专用,文本 tab 恒为 null)
239
243
  const [imgSize, setImgSize] = React.useState(null) // 图片自然尺寸 { w, h }(路径栏 meta)
240
244
  const [imgBroken, setImgBroken] = React.useState(false) // 图片解码失败(onError),显示占位与重试
@@ -804,6 +808,87 @@ export function EditorView(props) {
804
808
  }).catch((e) => setError('list异常:' + String(e)))
805
809
  }
806
810
 
811
+ /**
812
+ * Reject callbacks for a replaced file or loader generation.
813
+ * @private @author ddj 2026年09月28号
814
+ * @param path Normalized tab path.
815
+ * @param sequence Loader generation.
816
+ * @returns Whether this load still owns the active view.
817
+ */
818
+ function loadCurrent(path, sequence) {
819
+ return sequence === loadSeqRef.current && path === activeRef.current
820
+ }
821
+
822
+ /**
823
+ * Commit receipt metadata in the same React batch as loaded content.
824
+ * @private @author ddj 2026年09月28号
825
+ * @param path Loaded tab path.
826
+ * @param sid Session owning the operation.
827
+ * @param sequence Loader generation.
828
+ * @param failure Optional target-specific read/render error.
829
+ */
830
+ function stampLoad(path, sid, sequence, failure) {
831
+ if (!loadCurrent(path, sequence)) return
832
+ setReceiptLoad({ path, cwd, sessionId: sid, sequence, status: failure ? 'error' : 'ready', error: failure })
833
+ }
834
+
835
+ /**
836
+ * Report a read failure without attributing another operation's error to the file.
837
+ * @private @author ddj 2026年09月28号
838
+ * @param path Failed tab path.
839
+ * @param sid Session owning the load.
840
+ * @param sequence Loader generation.
841
+ * @param message Read failure description.
842
+ */
843
+ function failLoad(path, sid, sequence, message) {
844
+ if (!loadCurrent(path, sequence)) return
845
+ stampLoad(path, sid, sequence, message)
846
+ setLoadError(message)
847
+ setError(message)
848
+ setStatus('读取失败')
849
+ }
850
+
851
+ /**
852
+ * Attribute a preview decoder failure only to its still-current byte generation.
853
+ * @private @author ddj 2026年09月28号
854
+ * @param path Rendered file path.
855
+ * @param sid Rendering session.
856
+ * @param sequence Byte-load generation captured by the renderer.
857
+ * @param message Decoder failure.
858
+ */
859
+ function renderLoadFail(path, sid, sequence, message) {
860
+ if (!loadCurrent(path, sequence)) return
861
+ /** @author ddj 2026年09月28号 @param current Committed load state. @returns Correlated failure or unchanged state. */
862
+ const reject = (current) => acceptsLoad(current, sid, path, sequence, loadSeqRef.current)
863
+ ? { ...current, status: 'error', error: String(message) } : current
864
+ setReceiptLoad(reject)
865
+ }
866
+
867
+ /**
868
+ * Revalidate clean cached text without replacing edits made during the request.
869
+ * @private @author ddj 2026年09月28号
870
+ * @param model Cached text model.
871
+ * @param path Normalized target path.
872
+ * @param sid Session owning the read.
873
+ * @param sequence Loader generation.
874
+ * @param mark Existing disk-version bookkeeping callback.
875
+ */
876
+ function checkCached(model, path, sid, sequence, mark) {
877
+ if (dirtyRef.current[path]) return
878
+ const before = model.getValue()
879
+ /** @author ddj 2026年09月28号 @param res Disk content for a still-clean cached model. */
880
+ const accept = (res) => {
881
+ if (!loadCurrent(path, sequence) || dirtyRef.current[path] || model.getValue() !== before) return
882
+ if (!res?.ok) return
883
+ mark(res.version)
884
+ if (res.content === before) return
885
+ saveViewState(path)
886
+ setContent(res.content)
887
+ setStatus('已加载')
888
+ }
889
+ rpc('edrv.read', { sessionId: sid, path }).then(accept).catch(() => {})
890
+ }
891
+
807
892
  /**
808
893
  * 加载文件内容(对齐 VSCode model 复用:会话内已打开的 model 直接秒显,
809
894
  * 后台静默 RPC 校验防陈旧;首次打开走原读取流程)。
@@ -817,6 +902,8 @@ export function EditorView(props) {
817
902
  */
818
903
  const loadContent = (path, sid, force, version) => {
819
904
  const seq = ++loadSeqRef.current
905
+ setReceiptLoad({ path, cwd, sessionId: sid, sequence: seq, status: 'loading' })
906
+ setLoadError(null)
820
907
  /**
821
908
  * 记下磁盘版本基线;markRead=true 表示本次真的从磁盘读了内容
822
909
  * (已读版本台账随之失效,下一轮轮询按新版本重新比对)。
@@ -845,25 +932,18 @@ export function EditorView(props) {
845
932
  setContentPath(path)
846
933
  setLoadStage((prev) => ({ progress: Math.max(84, prev.progress), message: '文件已读取,准备创建编辑器…' }))
847
934
  setStatus('已加载')
848
- rpc('edrv.read', { sessionId: sid, path }).then((res) => {
849
- if (seq !== loadSeqRef.current || path !== active) return
850
- if (res && res.ok) mark(res.version)
851
- if (res && res.ok && res.content !== cachedModel.getValue()) {
852
- saveViewState(path) // 内容更新前保留当前视图位置
853
- setContent(res.content)
854
- setStatus('已加载')
855
- }
856
- }).catch(() => { /* 校验失败保留缓存内容 */ })
935
+ stampLoad(path, sid, seq)
936
+ checkCached(cachedModel, path, sid, seq, mark)
857
937
  return
858
938
  }
859
- setLoadError(null)
860
939
  setLoadStage({ progress: monaco ? 72 : 12, message: '读取文件内容…' })
861
940
  rpc('edrv.read', { sessionId: sid, path }).then((res) => {
862
- if (seq !== loadSeqRef.current || path !== active) return
941
+ if (!loadCurrent(path, seq)) return
863
942
  if (res && res.ok) {
864
943
  mark(res.version, true)
865
944
  setContent(res.content)
866
945
  setContentPath(path)
946
+ stampLoad(path, sid, seq)
867
947
  setLoadStage((prev) => ({ progress: Math.max(84, prev.progress), message: '文件已读取,准备创建编辑器…' }))
868
948
  setStatus('已加载')
869
949
  } else {
@@ -871,16 +951,12 @@ export function EditorView(props) {
871
951
  dapTrace('read-failed', { path, error: base, resolvedPath: res?.resolvedPath, cwd })
872
952
  // 带出 host 解析后的真实路径:跳转失败时一眼看出是路径解析错还是目标不存在
873
953
  const message = res?.resolvedPath ? base + ':' + relativeOf(String(res.resolvedPath), cwd) : base
874
- setLoadError(message)
875
- setError(message)
876
- setStatus('读取失败')
954
+ failLoad(path, sid, seq, message)
877
955
  }
878
956
  }).catch((e) => {
879
- if (seq !== loadSeqRef.current) return
957
+ if (!loadCurrent(path, seq)) return
880
958
  const message = 'read异常:' + String(e)
881
- setLoadError(message)
882
- setError(message)
883
- setStatus('读取失败')
959
+ failLoad(path, sid, seq, message)
884
960
  })
885
961
  }
886
962
 
@@ -898,12 +974,13 @@ export function EditorView(props) {
898
974
  setImgBroken(false)
899
975
  setLoadStage({ progress: monaco ? 72 : 12, message: '读取图片…' })
900
976
  readBinaryPreview(rpc, { sessionId: sid, path }).then((out) => {
901
- if (seq !== loadSeqRef.current || path !== active) return
977
+ if (!loadCurrent(path, seq)) return
902
978
  if (out.ok && out.via === 'binary') {
903
979
  // 新通道:原始字节本地组 data URL(线上省 base64,本地编码为 img src 所需不可省)
904
980
  recordBaseline(scope, path, out.version || version)
905
981
  clearReadVersion(scope, path) // 刚读过内容:已读版本台账失效,下一轮按新版本比对
906
982
  setImageSrc(dataUrlOf(bytesToBase64(out.bytes), out.mime))
983
+ stampLoad(path, sid, seq)
907
984
  setLoadStage({ progress: 100, message: '图片已就绪' })
908
985
  setStatus('已加载')
909
986
  return
@@ -911,15 +988,14 @@ export function EditorView(props) {
911
988
  noteBinFallback(sid, out)
912
989
  if (out.ok && out.via === 'base64') {
913
990
  if (!out.mime) {
914
- setLoadError('host 版本过旧:图片响应缺少 mime')
915
- setError('host 版本过旧:图片响应缺少 mime')
916
- setStatus('读取失败')
991
+ failLoad(path, sid, seq, 'host 版本过旧:图片响应缺少 mime')
917
992
  return
918
993
  }
919
994
  const imgVersion = out.version ?? version
920
995
  recordBaseline(scope, path, imgVersion)
921
996
  clearReadVersion(scope, path) // 刚读过内容:已读版本台账失效,下一轮按新版本比对
922
997
  setImageSrc(dataUrlOf(out.content, out.mime))
998
+ stampLoad(path, sid, seq)
923
999
  setLoadStage({ progress: 100, message: '图片已就绪' })
924
1000
  setStatus('已加载')
925
1001
  return
@@ -927,15 +1003,11 @@ export function EditorView(props) {
927
1003
  const base = out.error ? String(out.error) : '读取失败'
928
1004
  // 带出 host 解析后的真实路径:跳转失败时一眼看出是路径解析错还是目标不存在
929
1005
  const message = out.resolvedPath ? base + ':' + String(out.resolvedPath) : base
930
- setLoadError(message)
931
- setError(message)
932
- setStatus('读取失败')
1006
+ failLoad(path, sid, seq, message)
933
1007
  }).catch((e) => {
934
- if (seq !== loadSeqRef.current) return
1008
+ if (!loadCurrent(path, seq)) return
935
1009
  const message = 'read异常:' + String(e)
936
- setLoadError(message)
937
- setError(message)
938
- setStatus('读取失败')
1010
+ failLoad(path, sid, seq, message)
939
1011
  })
940
1012
  }
941
1013
 
@@ -959,12 +1031,13 @@ export function EditorView(props) {
959
1031
  cache.delete(path)
960
1032
  cache.set(path, hit)
961
1033
  setPdfBytes(base64ToBytes(hit))
1034
+ stampLoad(path, sid, seq)
962
1035
  setLoadStage({ progress: 100, message: 'PDF 已就绪' })
963
1036
  setStatus('已加载')
964
1037
  return
965
1038
  }
966
1039
  readBinaryPreview(rpc, { sessionId: sid, path }).then((out) => {
967
- if (seq !== loadSeqRef.current || path !== active) return
1040
+ if (!loadCurrent(path, seq)) return
968
1041
  if (out.ok && out.via === 'binary') {
969
1042
  const pdfVersion = out.version || version
970
1043
  recordBaseline(scope, path, pdfVersion)
@@ -973,6 +1046,7 @@ export function EditorView(props) {
973
1046
  cache.set(path, bytesToBase64(out.bytes))
974
1047
  while (cache.size > 6) cache.delete(cache.keys().next().value)
975
1048
  setPdfBytes(out.bytes)
1049
+ stampLoad(path, sid, seq)
976
1050
  setLoadStage({ progress: 100, message: 'PDF 已就绪' })
977
1051
  setStatus('已加载')
978
1052
  return
@@ -985,21 +1059,18 @@ export function EditorView(props) {
985
1059
  cache.set(path, out.content)
986
1060
  while (cache.size > 6) cache.delete(cache.keys().next().value)
987
1061
  setPdfBytes(base64ToBytes(out.content))
1062
+ stampLoad(path, sid, seq)
988
1063
  setLoadStage({ progress: 100, message: 'PDF 已就绪' })
989
1064
  setStatus('已加载')
990
1065
  return
991
1066
  }
992
1067
  const base = out.error ? String(out.error) : '读取失败'
993
1068
  const message = out.resolvedPath ? base + ':' + String(out.resolvedPath) : base
994
- setLoadError(message)
995
- setError(message)
996
- setStatus('读取失败')
1069
+ failLoad(path, sid, seq, message)
997
1070
  }).catch((e) => {
998
- if (seq !== loadSeqRef.current) return
1071
+ if (!loadCurrent(path, seq)) return
999
1072
  const message = 'read异常:' + String(e)
1000
- setLoadError(message)
1001
- setError(message)
1002
- setStatus('读取失败')
1073
+ failLoad(path, sid, seq, message)
1003
1074
  })
1004
1075
  }
1005
1076
 
@@ -1373,6 +1444,8 @@ export function EditorView(props) {
1373
1444
  setTimeout(() => window.dispatchEvent(new CustomEvent('edrv:search-focus')), 0)
1374
1445
  }
1375
1446
 
1447
+ React.useEffect(() => bindPanelCommand('searchInFiles', { sessionId, cwd: cwd ?? undefined }, openSearchPanel), [sessionId, cwd])
1448
+
1376
1449
  // Ctrl+Shift+F / Ctrl+Shift+V / Alt+←→ / Ctrl+Alt+←→ 的 capture 监听已随官方派发改造移除(见上)。
1377
1450
 
1378
1451
  // 整行上下移动的实现见下方「指令系统接线」effect(moveRow 单点定义,命令栏与键位共用)。
@@ -1457,7 +1530,6 @@ export function EditorView(props) {
1457
1530
  ['edrv.command.save', () => { if (editorRef.current?.getModel?.()) { flushSave(); doSaveRef.current?.(false) } }],
1458
1531
  // 快速打开由 QuickOpen 自己接该事件(它持有搜索框 ref),此处不重复实现
1459
1532
  ['edrv.command.toggleSidebar', () => setSidebarOn((v) => !v)],
1460
- ['edrv.command.searchInFiles', () => openSearchPanel()],
1461
1533
  ['edrv.command.toggleMarkdownPreview', () => {
1462
1534
  const path = activeRef.current
1463
1535
  if (!isMarkdownPath(path)) { setStatus('当前文件不是 Markdown'); return }
@@ -1753,6 +1825,23 @@ export function EditorView(props) {
1753
1825
  return true
1754
1826
  }
1755
1827
 
1828
+ /**
1829
+ * Apply a save completion only to the unchanged model that supplied its text.
1830
+ * @private @author ddj 2026年09月28号
1831
+ * @param path Saved tab path.
1832
+ * @param text Text actually sent to disk.
1833
+ * @param model Model captured before the save request.
1834
+ */
1835
+ function acceptSaved(path, text, model) {
1836
+ if (!model || modelsRef.current.get(path) !== model || model.isDisposed?.() || model.getValue() !== text) return
1837
+ setDirtyMap((current) => ({ ...current, [path]: false }))
1838
+ if (activeRef.current !== path) return
1839
+ setContent(text)
1840
+ setContentPath(path)
1841
+ setStatus('已保存 ' + new Date().toTimeString().slice(0, 8))
1842
+ setDiskFlag((prev) => (prev && prev.path === path ? null : prev))
1843
+ }
1844
+
1756
1845
  const doSave = (silent) => {
1757
1846
  if (!active) return
1758
1847
  // 外部改动待处理:自动保存(silent)直接放弃,避免陈旧缓冲覆盖磁盘;
@@ -1770,6 +1859,7 @@ export function EditorView(props) {
1770
1859
  if (!ed) return
1771
1860
  if (isImageActive) { setStatus('图片只读预览'); return }
1772
1861
  const text = ed.getValue()
1862
+ const savedModel = ed.getModel?.()
1773
1863
  const path = active
1774
1864
  if (!silent) setStatus('保存中…')
1775
1865
  // 在途登记:关闭路径的 persistDirty 据此跳过重复提交(内容在同一 tick 内取,必然相同)
@@ -1782,13 +1872,7 @@ export function EditorView(props) {
1782
1872
  recordBaseline(scope, path, res.rev)
1783
1873
  markReadVersion(scope, path, res.rev, true) // 缓冲即磁盘内容:同版本无需再读盘
1784
1874
  clearSync(scope, path)
1785
- setContent(text)
1786
- setContentPath(path)
1787
- setDirtyMap((d) => Object.assign({}, d, { [path]: false }))
1788
- if (path === active) {
1789
- setStatus('已保存 ' + new Date().toTimeString().slice(0, 8))
1790
- setDiskFlag((prev) => (prev && prev.path === path ? null : prev))
1791
- }
1875
+ acceptSaved(path, text, savedModel)
1792
1876
  refreshRecords()
1793
1877
  emitRefresh()
1794
1878
  // 片段配置文件保存后失效补全缓存(下次补全即读到新片段)
@@ -1879,7 +1963,7 @@ export function EditorView(props) {
1879
1963
  syncTypeSuggest(ed, monaco)
1880
1964
  restoreViewState(active)
1881
1965
  setLoadStage((prev) => ({ progress: Math.max(96, prev.progress), message: '创建编辑器视图…' }))
1882
- }, [monaco, active, content, contentReady, mdPreviewing])
1966
+ }, [monaco, active, content, contentReady, mdPreviewing, svnDiff])
1883
1967
 
1884
1968
  // PDF 面板外壳 ref 回调(div 仅在 PDF 分支渲染,refs 先于 effect 就绪)
1885
1969
  const ensurePdfHost = (node) => { pdfHostRef.current = node }
@@ -1889,21 +1973,32 @@ export function EditorView(props) {
1889
1973
  const host = pdfHostRef.current
1890
1974
  if (!isPdfActive || !pdfBytes || !host) return
1891
1975
  const path = active
1976
+ const sequence = receiptLoad?.sequence
1977
+ if (!acceptsLoad(receiptLoad, sessionId, path, sequence, loadSeqRef.current)) return
1978
+ let mounting = true
1979
+ /** @author ddj 2026年09月28号 @param message PDF error; only startup failures affect an open receipt. */
1980
+ const pdfError = (message) => {
1981
+ setError(message)
1982
+ if (mounting && message) renderLoadFail(path, sessionId, sequence, message)
1983
+ }
1984
+ /** @author ddj 2026年09月28号 End startup error attribution before later save operations. */
1985
+ const mounted = () => { mounting = false }
1892
1986
  const ctl = createPdfPanel({
1893
1987
  sessionId,
1894
1988
  path,
1895
1989
  setStatus,
1896
- setError,
1990
+ setError: pdfError,
1897
1991
  onDirtyChange: (d) => setDirtyMap((prev) => Object.assign({}, prev, { [path]: d })),
1898
1992
  onReload: () => loadContent(path, sessionId, true),
1899
1993
  })
1900
1994
  pdfCtlRef.current.set(path, ctl)
1901
- void ctl.mount(host, pdfBytes)
1995
+ void ctl.mount(host, pdfBytes).then(mounted, pdfError)
1902
1996
  return () => {
1997
+ mounting = false
1903
1998
  ctl.destroy()
1904
1999
  if (pdfCtlRef.current.get(path) === ctl) pdfCtlRef.current.delete(path)
1905
2000
  }
1906
- }, [isPdfActive, pdfBytes, active])
2001
+ }, [isPdfActive, pdfBytes, active, receiptLoad?.sequence])
1907
2002
 
1908
2003
  // 行内差异自绘(decorations / view zones / minus overlay)→ diffRenderer
1909
2004
  // ⚠️ 依赖含 mdPreviewing:切回源码时编辑器实例被重建(见上方 model 同步 effect 的说明),
@@ -1912,7 +2007,7 @@ export function EditorView(props) {
1912
2007
  if (!monaco || !editorRef.current || !active || content === null) return
1913
2008
  diffRendererRef.current.render(monaco, editorRef.current, pendingRegions, sessionId)
1914
2009
  setLoadStage({ progress: 100, message: '编辑器已就绪' })
1915
- }, [monaco, active, content, pendingRegions, mdPreviewing])
2010
+ }, [monaco, active, content, pendingRegions, mdPreviewing, svnDiff])
1916
2011
 
1917
2012
  React.useEffect(() => () => {
1918
2013
  flushSave()
@@ -2289,7 +2384,7 @@ export function EditorView(props) {
2289
2384
  ed.revealLineInCenter(Math.max(1, target.start ?? 1))
2290
2385
  ed.setPosition({ lineNumber: Math.max(1, target.start ?? 1), column: 1 })
2291
2386
  ed.focus()
2292
- }, [active, content, contentReady, pendingRegions, focusRequest, monaco, mdPreviewing])
2387
+ }, [active, content, contentReady, pendingRegions, focusRequest, monaco, mdPreviewing, svnDiff])
2293
2388
 
2294
2389
  const jumpTo = (region) => {
2295
2390
  if (editorRef.current) {
@@ -2473,6 +2568,16 @@ export function EditorView(props) {
2473
2568
  applyImgZoom(zoomStepOf(imgZoom ?? measured, dir))
2474
2569
  }
2475
2570
 
2571
+ /**
2572
+ * Preserve image UI behavior while attributing decoder failure to its rendered load.
2573
+ * @private @author ddj 2026年09月28号
2574
+ */
2575
+ function imageFailed() {
2576
+ if (!receiptLoad || !loadCurrent(receiptLoad.path, receiptLoad.sequence)) return
2577
+ setImgBroken(true)
2578
+ renderLoadFail(receiptLoad.path, receiptLoad.sessionId, receiptLoad.sequence, '图片解码失败')
2579
+ }
2580
+
2476
2581
  /**
2477
2582
  * 图片预览面板:工具条(尺寸 meta / 缩放 −+适应宽度 / SVG 文本切换 / 刷新)+ 棋盘底图片。
2478
2583
  * 初始适应宽度(等价 PDF page-width,0.1.7 官方统一缩放);按钮样式与 PDF 工具条同款。
@@ -2510,13 +2615,14 @@ export function EditorView(props) {
2510
2615
  : React.createElement('img', {
2511
2616
  className: 'edrv-imgview-img',
2512
2617
  ref: imgElRef,
2618
+ key: active + ':' + (receiptLoad?.sequence ?? 0),
2513
2619
  src: imageSrc,
2514
2620
  alt: active || '',
2515
2621
  style: (imgZoom !== null && imgSize)
2516
2622
  ? { width: Math.round(imgSize.w * imgZoom) + 'px', maxWidth: 'none', maxHeight: 'none' }
2517
2623
  : undefined,
2518
2624
  onLoad: (e) => { setImgBroken(false); setImgSize({ w: e.target.naturalWidth, h: e.target.naturalHeight }) },
2519
- onError: () => setImgBroken(true),
2625
+ onError: imageFailed,
2520
2626
  }))))
2521
2627
 
2522
2628
  /**
@@ -3118,13 +3224,89 @@ export function EditorView(props) {
3118
3224
  // G9:同 openFile,先归一再进页签,保证待跳转路径与 active 同形态
3119
3225
  const normalized = addTabNorm(path, true)
3120
3226
  if (!normalized) return
3227
+ stageFileAt(normalized, line, column, endLine, endColumn)
3228
+ }
3229
+
3230
+ /**
3231
+ * Stage a normalized navigation target using the existing reload-safe handoff.
3232
+ * @private @author ddj 2026年09月28号
3233
+ * @param normalized Exact tab path.
3234
+ * @param line Optional line.
3235
+ * @param column Optional column.
3236
+ * @param endLine Optional range end line.
3237
+ * @param endColumn Optional range end column.
3238
+ */
3239
+ function stageFileAt(normalized, line, column, endLine, endColumn) {
3121
3240
  pendingFocusRef.current = { path: normalized, region: null, line: line ?? null, column: column ?? 1, endLine: endLine ?? null, endColumn: endColumn ?? null }
3122
- // 第三次事故(2026-09-23):插件 HMR 热重载会重挂载本组件、挂载级 ref 随旧实例销毁 →
3123
- // 落点意图丢失。同步落 window 槽(跨热重载存活)做交接,落点成功后与 ref 一并清。
3124
3241
  putPendingNav({ path: normalized, line: line ?? null, column: column ?? 1, endLine: endLine ?? null, endColumn: endColumn ?? null })
3125
3242
  setFocusRequest((value) => value + 1)
3126
3243
  }
3127
3244
 
3245
+ /**
3246
+ * Open a receipt target without losing edits or leaving the requested source hidden.
3247
+ * @private @author ddj 2026年09月28号
3248
+ * @param request Claimed external open request.
3249
+ */
3250
+ function openReceiptFile(request) {
3251
+ if (!request.path) return
3252
+ let path = receiptTabPath(request.path, cwd)
3253
+ const existing = tabsRef.current.find((tab) => receiptPath(tab.path, cwd) === receiptPath(path, cwd))
3254
+ if (existing) path = existing.path
3255
+ flushSave()
3256
+ saveViewState(active)
3257
+ recordNav()
3258
+ setSvnDiff(null)
3259
+ const positioned = request.line != null || request.column != null
3260
+ if (positioned && mdPreviewRef.current.has(path)) toggleMdPreview(path)
3261
+ setError(null)
3262
+ if (monacoErr) setMonacoErr(null)
3263
+ addTab(path, true)
3264
+ if (positioned) stageFileAt(path, request.line ?? 1, request.column ?? 1)
3265
+ const moved = receiptLoad && receiptPath(receiptLoad.path, receiptLoad.cwd) !== receiptPath(path, cwd)
3266
+ if (path !== active || !(loadError || receiptLoad?.status === 'error' || moved)) return
3267
+ if (pdfCtlRef.current.get(path)?.isDirty?.()) return
3268
+ pdfB64CacheRef.current.delete(path)
3269
+ loadContent(path, sessionId, false)
3270
+ }
3271
+
3272
+ /**
3273
+ * Build facts from the current render; model identity is the exact cached instance.
3274
+ * @private @author ddj 2026年09月28号
3275
+ * @param request Claimed target and optional position.
3276
+ * @returns State consumed by the tested receipt predicates.
3277
+ */
3278
+ function receiptView(request) {
3279
+ const ed = editorRef.current
3280
+ const model = ed?.getModel?.()
3281
+ const modelMatches = Boolean(model && modelsRef.current.get(active) === model)
3282
+ let positionMatches = false
3283
+ if (modelMatches && (request.line != null || request.column != null)) {
3284
+ const expected = model.validatePosition({ lineNumber: request.line ?? 1, column: request.column ?? 1 })
3285
+ const actual = ed.getPosition()
3286
+ positionMatches = actual?.lineNumber === expected.lineNumber && actual?.column === expected.column
3287
+ }
3288
+ const image = imgElRef.current
3289
+ return {
3290
+ sessionId, cwd, active, sequence: loadSeqRef.current, load: receiptLoad,
3291
+ mounted: Boolean(viewRootRef.current), blocked: Boolean(svnDiff),
3292
+ kind: isImageActive ? 'image' : isPdfActive ? 'pdf' : mdPreviewing ? 'markdown' : 'text',
3293
+ contentPath, contentReady, modelMatches, positionMatches,
3294
+ imageLoaded: Boolean(imageSrc && !imgBroken && image?.getAttribute('src') === imageSrc && image.complete && image.naturalWidth > 0),
3295
+ pdfLoaded: Boolean(pdfBytes && pdfHostRef.current?.querySelector('.page[data-loaded="true"]')),
3296
+ engineError: monacoErr ? String(monacoErr) : null,
3297
+ }
3298
+ }
3299
+
3300
+ /** @private @author ddj 2026年09月28号 @param request Claimed target. @returns Correlated actual readiness. */
3301
+ function receiptReady(request) {
3302
+ return receiptReadyFor(request, receiptView(request))
3303
+ }
3304
+ /** @private @author ddj 2026年09月28号 @param request Claimed target. @returns Only its current load/render error. */
3305
+ function receiptError(request) {
3306
+ return receiptErrorFor(request, receiptView(request))
3307
+ }
3308
+ useOpenReceipt({ sessionId, open: openReceiptFile, ready: receiptReady, error: receiptError })
3309
+
3128
3310
  // 停帧跳转/调试命令监听(openFileAt 每渲染重建,经 ref 让空依赖监听读最新闭包)
3129
3311
  const dapOpenRef = React.useRef(null)
3130
3312
  dapOpenRef.current = openFileAt
@@ -3420,7 +3602,7 @@ export function EditorView(props) {
3420
3602
  } else if (isImageActive) {
3421
3603
  body = loadingBody(loadStage.message || '读取图片…', loadStage.progress)
3422
3604
  } else if (isPdfActive && pdfBytes) {
3423
- body = React.createElement('div', { className: 'edrv-pdf-host', ref: ensurePdfHost, key: active })
3605
+ body = React.createElement('div', { className: 'edrv-pdf-host', ref: ensurePdfHost, key: active + ':' + (receiptLoad?.sequence ?? 0) })
3424
3606
  } else if (isPdfActive) {
3425
3607
  body = loadingBody(loadStage.message || '读取 PDF…', loadStage.progress)
3426
3608
  } else if (mdPreviewing && contentReady) {
@@ -44,9 +44,11 @@ function parsePairs(text) {
44
44
  }))
45
45
  }
46
46
 
47
- /** 服务状态文案。 */
47
+ /** @author ddj 2026年09月28号 @param server Server lifecycle snapshot. @returns User-facing status. */
48
48
  function statusOf(server) {
49
- return server.status === 'connected' ? '在线' : server.status === 'connecting' ? '连接中' : server.status === 'disabled' ? '已禁用' : '错误'
49
+ if (!server.enabled || server.status === 'disabled') return '已禁用'
50
+ const labels = { connected: '在线', connecting: '装配中', configured: '已配置,等待会话', unverified: '已加载,连接未确认', error: '错误' }
51
+ return labels[server.status] ?? '状态未知'
50
52
  }
51
53
 
52
54
  /** MCP 服务卡片(全局与项目共用)。 */
@@ -60,7 +62,8 @@ function ServerCard({ server, onRefresh, onToggle, onRemove }) {
60
62
  React.createElement('button', { className: 'vsm-switch ' + (server.enabled ? 'on' : ''), onClick: () => onToggle(server), 'aria-label': server.enabled ? '禁用' : '启用' }, server.enabled ? '●' : '○'),
61
63
  ),
62
64
  ),
63
- React.createElement('div', { className: 'vsm-mcp-meta' }, statusOf(server), ' · ', server.toolCount, ' 个工具 · ', server.transport),
65
+ React.createElement('div', { className: 'vsm-mcp-meta' }, statusOf(server), ' · ', server.toolCount, ' 个工具 · ', server.transport,
66
+ server.instanceCount !== undefined ? ' · ' + server.instanceCount + ' 个会话实例(各自连接)' : ''),
64
67
  server.error && React.createElement('div', { className: 'vsm-mcp-error' }, server.error),
65
68
  React.createElement('div', { className: 'vsm-mcp-tools' }, server.tools.map((tool) => React.createElement('span', { key: tool.name, className: 'vsm-mcp-chip', title: tool.description || tool.name }, tool.name))),
66
69
  )
@@ -8,6 +8,7 @@
8
8
  * 作者 ddj 2026年09月09号 / 2026年09月10号
9
9
  */
10
10
  import React from 'react'
11
+ import { pickSessionScope } from '../sessionScope.js'
11
12
  import { EditorView } from './EditorView.js'
12
13
  import { setSideEditorMounted } from '../sidebarBridge.js'
13
14
  import { editorMountEpoch, markEditorActive, markEditorMounted, parseOfficialFileAddress, resolveNavLine, resolveNavOpen } from '../officialSidebar.js'
@@ -48,8 +49,10 @@ export function OfficialSideTab(props) {
48
49
  const schedule = props?.schedule
49
50
  const decide = () => {
50
51
  if (editorMountEpoch() !== epoch) return
51
- const current = props?.sessions?.list?.getSnapshot?.()?.current
52
- markEditorActive(current === self)
52
+ const current = typeof props?.readSessionScope === 'function'
53
+ ? props.readSessionScope().sessionId
54
+ : pickSessionScope({ list: props?.sessions?.list?.getSnapshot?.() }).sessionId
55
+ markEditorActive(Boolean(current && current !== self))
53
56
  }
54
57
  if (typeof schedule === 'function') schedule(decide, 0)
55
58
  else decide()
@@ -9,7 +9,8 @@
9
9
  */
10
10
  import React from 'react'
11
11
  import { rpc } from '../rpc.js'
12
- import { chordOf, useKeybindingsVersion } from '../keybindings.js'
12
+ import { bindPanelCommand } from '../panelCommands.js'
13
+ import { ariaOf, chordOf, useKeybindingsVersion } from '../keybindings.js'
13
14
  import { stepIndex } from './searchNav.js'
14
15
 
15
16
  /**
@@ -66,19 +67,18 @@ export function QuickOpen(props) {
66
67
  }
67
68
  // Esc 关闭(仅收起浮窗;历史 capture 行为保留,不参与命令派发、不吞键)
68
69
  const onEscape = (e) => {
69
- if (e.key !== 'Escape') return
70
+ if (e.isComposing || e.keyCode === 229 || e.getModifierState?.('AltGraph') || e.key !== 'Escape') return
70
71
  setOpen(false)
71
72
  inputRef.current?.blur?.()
72
73
  }
73
- // 快速打开键位(Ctrl+P)已归官方 shortcuts 机制派发:官方 resolve → 注册表 run →
74
- // 派发 `edrv.command.quickOpen` 事件到此处(历史窗口 capture 键位自检移除,防双开)。
74
+ // 官方与旧版键位统一走会话级命令接收器;挂载时只消费本会话的待打开请求。
75
75
  window.addEventListener('keydown', onEscape, true)
76
- window.addEventListener('edrv.command.quickOpen', openBox)
76
+ const offCommand = bindPanelCommand('quickOpen', { sessionId }, openBox)
77
77
  return () => {
78
78
  window.removeEventListener('keydown', onEscape, true)
79
- window.removeEventListener('edrv.command.quickOpen', openBox)
79
+ offCommand()
80
80
  }
81
- }, [])
81
+ }, [sessionId])
82
82
 
83
83
  // 高亮项滚入浮窗可视区(候选集或高亮变化时;nearest 避免整页跳动)
84
84
  React.useEffect(() => {
@@ -99,7 +99,7 @@ export function QuickOpen(props) {
99
99
  * @param e 输入框键盘事件
100
100
  */
101
101
  const onInputKey = (e) => {
102
- if (e.nativeEvent?.isComposing) return
102
+ if (e.nativeEvent?.isComposing || e.nativeEvent?.keyCode === 229 || e.getModifierState?.('AltGraph')) return
103
103
  if (e.key === 'Escape') { setOpen(false); return }
104
104
  if (!showPop || !list.length) return
105
105
  if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
@@ -135,7 +135,8 @@ export function QuickOpen(props) {
135
135
 
136
136
  return React.createElement('div', { className: 'edrv-search-wrap' },
137
137
  React.createElement('input', {
138
- ref: inputRef, className: 'edrv-search', placeholder: '搜索文件 (' + (chordOf('edrv.quickOpen') ?? 'Ctrl+P') + ')',
138
+ ref: inputRef, className: 'edrv-search', placeholder: '搜索文件' + (chordOf('edrv.quickOpen') ? ' (' + chordOf('edrv.quickOpen') + ')' : ''),
139
+ 'aria-keyshortcuts': ariaOf('edrv.quickOpen'),
139
140
  value: q,
140
141
  role: 'combobox',
141
142
  'aria-expanded': showPop && list.length > 0,