dsh-vscode-mode 0.1.60 → 0.1.61
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 +356 -88
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +19 -1
- package/src/client/navHistory.ts +28 -1
- package/src/client/officialSidebar.ts +72 -1
- package/src/client/paths.ts +3 -3
- package/src/client/sidebar/panels/FileExplorer.ts +14 -11
- package/src/client/sidebar/panels/SearchPanel.ts +10 -7
- package/src/client/sidebar/types.ts +2 -0
- package/src/client/state/explorerCache.ts +9 -9
- package/src/client/state/explorerEntriesCache.ts +31 -31
- package/src/client/state/modelCache.ts +77 -0
- package/src/client/state/scopeStore.ts +96 -0
- package/src/client/state/viewStateCache.ts +9 -9
- package/src/client/ui/EditorView.ts +57 -26
- package/src/client/ui/OfficialSideTab.ts +21 -2
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 编辑区状态作用域键(scope key)。
|
|
3
|
+
* 编辑区 UI 状态(页签/展开树/视图状态/侧边栏等)按「工作区」隔离:同一工作区下
|
|
4
|
+
* 切换对话恢复同一份状态,无体感差别;会话没有 cwd 时回退按会话隔离(旧行为)。
|
|
5
|
+
* 另提供旧「会话键」数据到「工作区键」的一次性迁移(只复制不删除,幂等)。
|
|
6
|
+
* 键前缀统一走 paths.ts PathConst(CACHE_KEY),本模块只产出拼接用的作用域串。
|
|
7
|
+
* 作者 ddj 2026-09-09
|
|
8
|
+
*/
|
|
9
|
+
import { CACHE_KEY } from '../paths.js'
|
|
10
|
+
|
|
11
|
+
// --region 归一化与作用域键
|
|
12
|
+
|
|
13
|
+
/** Windows 盘符绝对路径(如 D:/ 或 d:\),大小写不敏感。 */
|
|
14
|
+
const WIN_DRIVE_RE = /^[a-z]:\//i
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 归一化工作区路径:trim、反斜杠→斜杠、去尾部斜杠;
|
|
18
|
+
* Windows 盘符绝对路径整体小写(文件系统大小写不敏感),POSIX 路径保留大小写。
|
|
19
|
+
* @author ddj 2026年09月09号
|
|
20
|
+
* @param cwd 工作区目录(sessions 快照 byId[sessionId].cwd)
|
|
21
|
+
* @returns 归一化路径;空值返回 ''
|
|
22
|
+
*/
|
|
23
|
+
export function normalizeWsPath(cwd: string | undefined | null): string {
|
|
24
|
+
const s = String(cwd ?? '').trim().replace(/\\/g, '/').replace(/\/+$/, '')
|
|
25
|
+
if (!s) return ''
|
|
26
|
+
return WIN_DRIVE_RE.test(s) ? s.toLowerCase() : s
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 计算编辑区状态作用域键:有 cwd → 'ws:' + 归一化路径(同工作区共享);
|
|
31
|
+
* 无 cwd → 'sid:' + sessionId(回退按会话隔离,对齐旧行为)。
|
|
32
|
+
* @author ddj 2026年09月09号
|
|
33
|
+
* @param cwd 工作区目录(可为 null/undefined)
|
|
34
|
+
* @param sessionId 会话 id(cwd 缺失时的隔离键)
|
|
35
|
+
* @returns 作用域键字符串
|
|
36
|
+
*/
|
|
37
|
+
export function workspaceScopeOf(cwd: string | undefined | null, sessionId: string | undefined): string {
|
|
38
|
+
const ws = normalizeWsPath(cwd)
|
|
39
|
+
if (ws) return 'ws:' + ws
|
|
40
|
+
return 'sid:' + String(sessionId ?? '')
|
|
41
|
+
}
|
|
42
|
+
// --endregion
|
|
43
|
+
|
|
44
|
+
// --region 旧会话键迁移
|
|
45
|
+
|
|
46
|
+
/** 参与迁移的 localStorage 键前缀(编辑区全部按会话持久化的旧键)。 */
|
|
47
|
+
const MIGRATE_PREFIXES: readonly string[] = [
|
|
48
|
+
CACHE_KEY.editor,
|
|
49
|
+
CACHE_KEY.viewstate,
|
|
50
|
+
CACHE_KEY.sidebar + 'side.',
|
|
51
|
+
CACHE_KEY.sidebar,
|
|
52
|
+
CACHE_KEY.search,
|
|
53
|
+
CACHE_KEY.expanded,
|
|
54
|
+
CACHE_KEY.entries,
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
/** 已迁移过的 scope+sessionId 组合(按 storage 实例分组;每页生命周期只跑一次)。 */
|
|
58
|
+
const migratedPairs = new WeakMap<object, Set<string>>()
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 把旧「前缀+sessionId」键的数据一次性复制到「前缀+scope」键(目标已存在则跳过)。
|
|
62
|
+
* 只复制不删除旧键;幂等,可安全重复调用。升级后首个工作区键为空时兜底恢复旧数据。
|
|
63
|
+
* @author ddj 2026年09月09号
|
|
64
|
+
* @param scope 目标作用域键
|
|
65
|
+
* @param sessionId 旧数据所属会话 id
|
|
66
|
+
* @param storage localStorage 注入口(缺省 window.localStorage,测试可传内存桩)
|
|
67
|
+
*/
|
|
68
|
+
export function migrateScopedKeys(scope: string, sessionId: string | undefined, storage?: Storage): void {
|
|
69
|
+
if (!scope || !sessionId) return
|
|
70
|
+
let store: Storage | null = null
|
|
71
|
+
try {
|
|
72
|
+
store = storage ?? window.localStorage
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
if (!store) return
|
|
77
|
+
let pairs = migratedPairs.get(store)
|
|
78
|
+
if (!pairs) {
|
|
79
|
+
pairs = new Set()
|
|
80
|
+
migratedPairs.set(store, pairs)
|
|
81
|
+
}
|
|
82
|
+
const pair = scope + '|' + sessionId
|
|
83
|
+
if (pairs.has(pair)) return
|
|
84
|
+
pairs.add(pair)
|
|
85
|
+
const legacy = String(sessionId)
|
|
86
|
+
for (const prefix of MIGRATE_PREFIXES) {
|
|
87
|
+
const target = prefix + scope
|
|
88
|
+
try {
|
|
89
|
+
if (store.getItem(target) !== null) continue
|
|
90
|
+
const raw = store.getItem(prefix + legacy)
|
|
91
|
+
if (raw === null) continue
|
|
92
|
+
store.setItem(target, raw)
|
|
93
|
+
} catch (error) { /* 配额满/隐私模式忽略,不阻塞 */ }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// --endregion
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { CACHE_KEY } from '../paths.js'
|
|
9
9
|
|
|
10
|
-
/** localStorage
|
|
10
|
+
/** localStorage 键前缀(按工作区作用域隔离,无 cwd 会话回退会话键)。 */
|
|
11
11
|
const KEY_PREFIX = CACHE_KEY.viewstate
|
|
12
12
|
|
|
13
13
|
/** 视图状态条目容量上限。 */
|
|
@@ -95,27 +95,27 @@ export function upsertViewState(
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
|
-
*
|
|
98
|
+
* 读取某作用域的视图状态缓存(localStorage 不可用 → 空映射)。
|
|
99
99
|
* @author ddj 2026年08月28号
|
|
100
|
-
* @param
|
|
100
|
+
* @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
|
|
101
101
|
* @returns path → viewState 映射
|
|
102
102
|
*/
|
|
103
|
-
export function viewStatesLoad(
|
|
103
|
+
export function viewStatesLoad(scope: string): Record<string, unknown> {
|
|
104
104
|
try {
|
|
105
|
-
return parseViewStates(window.localStorage.getItem(KEY_PREFIX + String(
|
|
105
|
+
return parseViewStates(window.localStorage.getItem(KEY_PREFIX + String(scope))) ?? {}
|
|
106
106
|
} catch (error) {
|
|
107
107
|
return {}
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
*
|
|
112
|
+
* 写入某作用域的视图状态缓存(损坏/配额满 → 忽略)。
|
|
113
113
|
* @author ddj 2026年08月28号
|
|
114
|
-
* @param
|
|
114
|
+
* @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
|
|
115
115
|
* @param states path → viewState 映射
|
|
116
116
|
*/
|
|
117
|
-
export function viewStatesSave(
|
|
117
|
+
export function viewStatesSave(scope: string, states: Record<string, unknown>): void {
|
|
118
118
|
try {
|
|
119
|
-
window.localStorage.setItem(KEY_PREFIX + String(
|
|
119
|
+
window.localStorage.setItem(KEY_PREFIX + String(scope), serializeViewStates(states))
|
|
120
120
|
} catch (error) { /* 配额满/隐私模式忽略 */ }
|
|
121
121
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* dsh-vscode-mode client — EditorView:中央 VSCode 式文件编辑器(编排层)。
|
|
4
4
|
* 迁移自原 src/client/index.ts 的 EditorView,语义不改;差异自绘已抽到 monaco/diffRender。
|
|
5
5
|
* 职责:页签/QuickOpen/Monaco 编辑器/差异审查(DiffBox/Launcher/Badge 事件装配)/状态栏/自动保存。
|
|
6
|
+
* 状态作用域:页签/视图状态/侧边栏/展开树等 UI 状态按工作区作用域(cwd)隔离,
|
|
7
|
+
* 同一工作区切换对话恢复同一份状态;无 cwd 会话回退按会话隔离(scopeStore)。
|
|
6
8
|
* 作者 ddj 2026-08-20
|
|
7
9
|
*/
|
|
8
10
|
import React from 'react'
|
|
@@ -25,9 +27,11 @@ import { editorHeight } from '../editorLayout.js'
|
|
|
25
27
|
import { revealInExplorer as revealPathInExplorer } from '../fileReveal.js'
|
|
26
28
|
import { setSidePending, SIDEBAR_INSTALL_CMD } from '../sidebarBridge.js'
|
|
27
29
|
import { upsertViewState, viewStatesLoad, viewStatesSave } from '../state/viewStateCache.js'
|
|
30
|
+
import { migrateScopedKeys, workspaceScopeOf } from '../state/scopeStore.js'
|
|
31
|
+
import { modelsForScope, rememberModel } from '../state/modelCache.js'
|
|
28
32
|
import { bindingsOf, chordOf, matchEvent, useKeybindingsVersion } from '../keybindings.js'
|
|
29
33
|
import { getSidebarMinWidth } from '../sidebarMin.js'
|
|
30
|
-
import {
|
|
34
|
+
import { navHistoryFor } from '../navHistory.js'
|
|
31
35
|
import { statusOfAdd } from '../addToConversation.js'
|
|
32
36
|
import { CACHE_KEY } from '../paths.js'
|
|
33
37
|
import { bindLspEditor, runGoToDefinition, runFindReferences, hideReferencesOverlay } from '../monaco/lsp/providers.js'
|
|
@@ -49,6 +53,22 @@ export function EditorView(props) {
|
|
|
49
53
|
const addToConversation = props.addToConversation
|
|
50
54
|
const layout = props?.layout === 'side' ? 'side' : 'tab'
|
|
51
55
|
const sideHint = props?.sideHint
|
|
56
|
+
// 当前会话工作区 cwd(sessions 快照反应式读取;cwd 变化时作用域跟随刷新)
|
|
57
|
+
const sessionsList = props?.sessions?.list
|
|
58
|
+
const cwd = React.useSyncExternalStore(
|
|
59
|
+
React.useCallback((onStoreChange) => (
|
|
60
|
+
typeof sessionsList?.subscribe === 'function' ? sessionsList.subscribe(onStoreChange) : () => {}
|
|
61
|
+
), [sessionsList]),
|
|
62
|
+
() => sessionsList?.getSnapshot?.()?.byId?.[sessionId]?.cwd ?? null,
|
|
63
|
+
() => null,
|
|
64
|
+
)
|
|
65
|
+
// 状态作用域(scopeStore):同工作区共享一份编辑区状态,无 cwd 回退会话隔离;
|
|
66
|
+
// 迁移放渲染期(useMemo)——子面板恢复 effect 先于父 effect 执行,须先补齐工作区键
|
|
67
|
+
const scope = React.useMemo(() => {
|
|
68
|
+
const s = workspaceScopeOf(cwd, sessionId)
|
|
69
|
+
migrateScopedKeys(s, sessionId)
|
|
70
|
+
return s
|
|
71
|
+
}, [cwd, sessionId])
|
|
52
72
|
const [monaco, setMonaco] = React.useState(null)
|
|
53
73
|
const [monacoErr, setMonacoErr] = React.useState(null)
|
|
54
74
|
const [records, setRecords] = React.useState({})
|
|
@@ -93,17 +113,19 @@ export function EditorView(props) {
|
|
|
93
113
|
const editorRef = React.useRef(null)
|
|
94
114
|
const viewRootRef = React.useRef(null)
|
|
95
115
|
const monacoRef = React.useRef(null)
|
|
96
|
-
const modelsRef = React.useRef(
|
|
116
|
+
const modelsRef = React.useRef(null)
|
|
117
|
+
// model 跨挂载缓存(按工作区作用域):重挂载秒显内容,消除切换对话闪烁
|
|
118
|
+
if (!modelsRef.current) modelsRef.current = modelsForScope(scope)
|
|
97
119
|
// 视图状态缓存(path → Monaco viewState;重启后恢复光标/滚动/折叠位置)
|
|
98
120
|
const viewStatesRef = React.useRef({})
|
|
99
121
|
const saveTimerRef = React.useRef(null)
|
|
100
122
|
const loadSeqRef = React.useRef(0)
|
|
101
123
|
const programmaticRef = React.useRef(false)
|
|
102
|
-
const
|
|
124
|
+
const restoredScopeRef = React.useRef(null) // 已恢复状态的作用域(cwd 晚到 sid→ws 时允许重恢复)
|
|
103
125
|
const pendingFocusRef = React.useRef(null) // { path, region } 内容加载后跳转
|
|
104
126
|
// 导航历史(后退/前进):跨文件焦点位置;按会话隔离,会话切换重置
|
|
105
127
|
const navRef = React.useRef(null)
|
|
106
|
-
if (!navRef.current) navRef.current =
|
|
128
|
+
if (!navRef.current) navRef.current = navHistoryFor(scope)
|
|
107
129
|
const navPendingRef = React.useRef(null) // { path,line,column,viewState } 待恢复条目(内容就绪后消费)
|
|
108
130
|
const navCursorTimerRef = React.useRef(null) // 光标位置防抖记录计时器
|
|
109
131
|
const navBackRef = React.useRef(null) // 后退动作最新闭包(窗口级键盘监听读取)
|
|
@@ -120,6 +142,7 @@ export function EditorView(props) {
|
|
|
120
142
|
const batchBusyRef = React.useRef(false) // 批量 Keep All/Undo All 防重入
|
|
121
143
|
const menuHandlersRef = React.useRef(null) // 右键菜单动作的最新闭包(Monaco addAction 空依赖回调读取)
|
|
122
144
|
const doSaveRef = React.useRef(null) // 保存动作的最新闭包(窗口级保存监听读取)
|
|
145
|
+
const saveViewStateRef = React.useRef(null) // 视图状态保存的最新闭包(卸载清理读取,避免过期 active)
|
|
123
146
|
const diffRendererRef = React.useRef(null)
|
|
124
147
|
const layoutRef = React.useRef(layout) // ensureEditor 空依赖闭包读取的稳定布局
|
|
125
148
|
layoutRef.current = layout
|
|
@@ -163,7 +186,7 @@ export function EditorView(props) {
|
|
|
163
186
|
}
|
|
164
187
|
|
|
165
188
|
/**
|
|
166
|
-
*
|
|
189
|
+
* 保存当前活动文件的视图状态(光标/滚动/折叠)到工作区作用域缓存。
|
|
167
190
|
* @author ddj 2026年08月28号
|
|
168
191
|
* @param path 要保存的文件路径(缺省 = 当前 active)
|
|
169
192
|
*/
|
|
@@ -175,9 +198,10 @@ export function EditorView(props) {
|
|
|
175
198
|
const state = ed.saveViewState()
|
|
176
199
|
if (!state) return
|
|
177
200
|
viewStatesRef.current = upsertViewState(viewStatesRef.current, target, state)
|
|
178
|
-
viewStatesSave(
|
|
201
|
+
viewStatesSave(scope, viewStatesRef.current)
|
|
179
202
|
} catch (e) { /* 视图状态保存失败忽略 */ }
|
|
180
203
|
}
|
|
204
|
+
saveViewStateRef.current = saveViewState
|
|
181
205
|
|
|
182
206
|
/**
|
|
183
207
|
* 恢复指定文件的视图状态(内容就绪、setModel 后调用;恢复后即消费删除)。
|
|
@@ -618,13 +642,15 @@ export function EditorView(props) {
|
|
|
618
642
|
}
|
|
619
643
|
}, [sessionId, layout])
|
|
620
644
|
|
|
621
|
-
// localStorage v2
|
|
645
|
+
// localStorage v2 恢复页签(按工作区作用域;cwd 晚到 sid→ws 切换时重恢复一次)
|
|
622
646
|
React.useEffect(() => {
|
|
623
|
-
if (
|
|
624
|
-
|
|
625
|
-
|
|
647
|
+
if (!sessionId || restoredScopeRef.current === scope) return
|
|
648
|
+
restoredScopeRef.current = scope
|
|
649
|
+
// model 缓存跟随作用域(切工作区释放旧作用域模型;同工作区跨挂载复用)
|
|
650
|
+
modelsRef.current = modelsForScope(scope)
|
|
651
|
+
viewStatesRef.current = viewStatesLoad(scope)
|
|
626
652
|
try {
|
|
627
|
-
const raw = localStorage.getItem(CACHE_KEY.editor + String(
|
|
653
|
+
const raw = localStorage.getItem(CACHE_KEY.editor + String(scope))
|
|
628
654
|
if (raw) {
|
|
629
655
|
const saved = JSON.parse(raw)
|
|
630
656
|
if (Array.isArray(saved.tabs) && saved.tabs.length) {
|
|
@@ -633,25 +659,25 @@ export function EditorView(props) {
|
|
|
633
659
|
}
|
|
634
660
|
}
|
|
635
661
|
} catch (e) { /* 损坏忽略 */ }
|
|
636
|
-
}, [sessionId])
|
|
662
|
+
}, [sessionId, scope])
|
|
637
663
|
|
|
638
|
-
//
|
|
664
|
+
// 导航历史按工作区作用域隔离:同工作区切换对话保留历史,跨工作区换实例
|
|
639
665
|
React.useEffect(() => {
|
|
640
|
-
navRef.current =
|
|
666
|
+
navRef.current = navHistoryFor(scope)
|
|
641
667
|
navPendingRef.current = null
|
|
642
668
|
if (navCursorTimerRef.current) { clearTimeout(navCursorTimerRef.current); navCursorTimerRef.current = null }
|
|
643
669
|
setNavTick((v) => v + 1)
|
|
644
|
-
}, [
|
|
670
|
+
}, [scope])
|
|
645
671
|
|
|
646
672
|
React.useEffect(() => {
|
|
647
673
|
if (!sessionId) return
|
|
648
|
-
try { localStorage.setItem(CACHE_KEY.editor + String(
|
|
674
|
+
try { localStorage.setItem(CACHE_KEY.editor + String(scope), JSON.stringify({ tabs: tabs.map((t) => t.path), active })) }
|
|
649
675
|
catch (e) { /* 忽略 */ }
|
|
650
|
-
}, [tabs, active, sessionId])
|
|
676
|
+
}, [tabs, active, sessionId, scope])
|
|
651
677
|
|
|
652
678
|
// 侧边栏状态:恢复(显隐/宽度/激活面板);侧栏形态独立键(默认收起,不共享页签形态偏好)
|
|
653
679
|
// 宽度下限来自通用设置 sidebarMinWidth(默认 300),恢复值按其重夹
|
|
654
|
-
const sidebarKey = CACHE_KEY.sidebar + (layout === 'side' ? 'side.' : '') + String(
|
|
680
|
+
const sidebarKey = CACHE_KEY.sidebar + (layout === 'side' ? 'side.' : '') + String(scope)
|
|
655
681
|
React.useEffect(() => {
|
|
656
682
|
if (!sessionId) return
|
|
657
683
|
try {
|
|
@@ -682,7 +708,6 @@ export function EditorView(props) {
|
|
|
682
708
|
try { localStorage.setItem(sidebarKey, JSON.stringify({ on: sidebarOn, width: sidebarW, panel: activePanel })) }
|
|
683
709
|
catch (e) { /* 忽略 */ }
|
|
684
710
|
}, [sidebarOn, sidebarW, activePanel, sessionId, sidebarKey])
|
|
685
|
-
|
|
686
711
|
// 切换侧边栏(capture 抢占,避免与 DSH 全局冲突;键位随快捷键配置)
|
|
687
712
|
React.useEffect(() => {
|
|
688
713
|
const onKey = (e) => {
|
|
@@ -836,8 +861,10 @@ export function EditorView(props) {
|
|
|
836
861
|
const cache = modelsRef.current
|
|
837
862
|
let model = cache.get(path)
|
|
838
863
|
if (!model) {
|
|
839
|
-
|
|
840
|
-
|
|
864
|
+
// 注册表守卫:同 URI 已存在的 model 直接复用(HMR 换 bundle 后防重复创建抛错)
|
|
865
|
+
const uri = window.monaco.Uri.parse('edrv:///' + encodeURI(path))
|
|
866
|
+
model = window.monaco.editor.getModel(uri) || window.monaco.editor.createModel(text ?? '', langOf(path), uri)
|
|
867
|
+
rememberModel(cache, path, model)
|
|
841
868
|
} else if (text !== undefined && model.getValue() !== text) {
|
|
842
869
|
programmaticRef.current = true
|
|
843
870
|
model.setValue(text)
|
|
@@ -930,12 +957,14 @@ export function EditorView(props) {
|
|
|
930
957
|
|
|
931
958
|
React.useEffect(() => () => {
|
|
932
959
|
flushSave()
|
|
933
|
-
|
|
960
|
+
// 卸载即切作用域/重挂载:先把待记录光标落进历史,再存当前文件视图状态
|
|
961
|
+
//(经 ref 取最新闭包,修正旧实现捕获挂载时 active 的问题)
|
|
962
|
+
flushNavCursor()
|
|
963
|
+
saveViewStateRef.current?.()
|
|
934
964
|
diffRendererRef.current?.dispose?.()
|
|
935
965
|
hideReferencesOverlay()
|
|
936
966
|
if (editorRef.current) { editorRef.current.dispose(); editorRef.current = null }
|
|
937
|
-
|
|
938
|
-
modelsRef.current.clear()
|
|
967
|
+
// model 不在此销毁:跨挂载缓存按作用域存活(modelCache 切作用域时统一释放)
|
|
939
968
|
for (const ctl of pdfCtlRef.current.values()) ctl.destroy()
|
|
940
969
|
pdfCtlRef.current.clear()
|
|
941
970
|
const root = editorRef.current && editorRef.current.getDomNode ? editorRef.current.getDomNode() : null
|
|
@@ -1683,8 +1712,10 @@ export function EditorView(props) {
|
|
|
1683
1712
|
const sidebarPanels = props.sidebarPanels
|
|
1684
1713
|
const sidebarCtx = {
|
|
1685
1714
|
sessionId,
|
|
1686
|
-
//
|
|
1687
|
-
cwd:
|
|
1715
|
+
// 当前会话工作区(反应式 cwd;规则面板项目 Tab 自动匹配)
|
|
1716
|
+
cwd: cwd ?? null,
|
|
1717
|
+
// 状态作用域键(工作区优先,无 cwd 回退会话;面板持久化统一走它)
|
|
1718
|
+
scope,
|
|
1688
1719
|
openFile: (p) => openFile(p, false),
|
|
1689
1720
|
openFileAt,
|
|
1690
1721
|
activePath: active,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import React from 'react'
|
|
12
12
|
import { EditorView } from './EditorView.js'
|
|
13
13
|
import { setSideEditorMounted } from '../sidebarBridge.js'
|
|
14
|
-
import { parseOfficialFileAddress, resolveNavOpen } from '../officialSidebar.js'
|
|
14
|
+
import { editorMountEpoch, markEditorMounted, parseOfficialFileAddress, resolveNavOpen } from '../officialSidebar.js'
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* 官方侧边栏 Tab 正文组件(keyed slot sidebar.right.pane.tab 装配)。
|
|
@@ -32,10 +32,29 @@ export function OfficialSideTab(props) {
|
|
|
32
32
|
}
|
|
33
33
|
const revision = typeof navigation?.revision === 'number' ? navigation.revision : -1
|
|
34
34
|
const revisionRef = React.useRef(-1)
|
|
35
|
+
// 最新 sessionId 镜像(卸载清理判读「切会话 vs 本会话关闭」用)
|
|
36
|
+
const sessionIdRef = React.useRef(sessionId)
|
|
37
|
+
sessionIdRef.current = sessionId
|
|
35
38
|
|
|
39
|
+
// 编辑 Tab 激活标记:挂载即激活;卸载时延迟判定「切会话(保持,供新会话自动恢复)
|
|
40
|
+
// vs 本会话关闭/切走其他 Tab(清除,尊重用户选择)」——通知早于会话状态落定,必须延后读;
|
|
41
|
+
// 纪元守卫:判定期间有新正文挂载(HMR/自动恢复)则放弃清除
|
|
36
42
|
React.useEffect(() => {
|
|
37
43
|
setSideEditorMounted(true)
|
|
38
|
-
|
|
44
|
+
markEditorMounted()
|
|
45
|
+
return () => {
|
|
46
|
+
setSideEditorMounted(false)
|
|
47
|
+
const epoch = editorMountEpoch()
|
|
48
|
+
const self = sessionIdRef.current
|
|
49
|
+
const schedule = props?.schedule
|
|
50
|
+
const decide = () => {
|
|
51
|
+
if (editorMountEpoch() !== epoch) return
|
|
52
|
+
const current = props?.sessions?.list?.getSnapshot?.()?.current
|
|
53
|
+
markEditorActive(current === self)
|
|
54
|
+
}
|
|
55
|
+
if (typeof schedule === 'function') schedule(decide, 0)
|
|
56
|
+
else decide()
|
|
57
|
+
}
|
|
39
58
|
}, [])
|
|
40
59
|
|
|
41
60
|
React.useEffect(() => {
|