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 CHANGED
@@ -3058,7 +3058,7 @@ window.__ModuleLoader__.load({
3058
3058
  * 浏览器不涉 ~/.dsh 文件系统,这里只管键名 + shared 路由常量。
3059
3059
  * 作者 ddj 2026-09-01
3060
3060
  */
3061
- /** localStorage 键前缀表(按功能隔离;均按会话拼接)。 */
3061
+ /** localStorage 键前缀表(按功能隔离;与 scopeStore 作用域键拼接:工作区优先,无 cwd 回退会话)。 */
3062
3062
  const CACHE_KEY = {
3063
3063
  /** 展开状态(explorerCache v1,当前使用)。 */
3064
3064
  expanded: "edrv.cache.explorer.v1.",
@@ -3072,7 +3072,7 @@ window.__ModuleLoader__.load({
3072
3072
  sidebar: "edrv.sidebar.",
3073
3073
  /** 搜索面板条件(SearchPanel v1)。 */
3074
3074
  search: "edrv.search.v1.",
3075
- /** 侧边栏提示已关闭标记(全局,无会话)。 */
3075
+ /** 侧边栏提示已关闭标记(全局,无作用域)。 */
3076
3076
  sideHint: "edrv.side-hint-dismissed",
3077
3077
  /** 「性能优化」页工作区栏目展开状态(workspaceFoldCache v1,全局不按会话)。 */
3078
3078
  workspaceFold: "edrv.ws-fold.v1.",
@@ -3088,7 +3088,7 @@ window.__ModuleLoader__.load({
3088
3088
  * 键前缀统一走 paths.ts PathConst(CACHE_KEY.viewstate)。
3089
3089
  * 作者 ddj 2026-08-28
3090
3090
  */
3091
- /** localStorage 键前缀(按会话隔离)。 */
3091
+ /** localStorage 键前缀(按工作区作用域隔离,无 cwd 会话回退会话键)。 */
3092
3092
  const KEY_PREFIX$2 = CACHE_KEY.viewstate;
3093
3093
  /**
3094
3094
  * 判断对象是否为合法的 Monaco view state(保守形状校验,非深校验)。
@@ -3160,29 +3160,173 @@ window.__ModuleLoader__.load({
3160
3160
  return next;
3161
3161
  }
3162
3162
  /**
3163
- * 读取某会话的视图状态缓存(localStorage 不可用 → 空映射)。
3163
+ * 读取某作用域的视图状态缓存(localStorage 不可用 → 空映射)。
3164
3164
  * @author ddj 2026年08月28号
3165
- * @param sessionId 会话 id
3165
+ * @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
3166
3166
  * @returns path → viewState 映射
3167
3167
  */
3168
- function viewStatesLoad(sessionId) {
3168
+ function viewStatesLoad(scope) {
3169
3169
  try {
3170
- return parseViewStates(window.localStorage.getItem(KEY_PREFIX$2 + String(sessionId))) ?? {};
3170
+ return parseViewStates(window.localStorage.getItem(KEY_PREFIX$2 + String(scope))) ?? {};
3171
3171
  } catch (error) {
3172
3172
  return {};
3173
3173
  }
3174
3174
  }
3175
3175
  /**
3176
- * 写入某会话的视图状态缓存(损坏/配额满 → 忽略)。
3176
+ * 写入某作用域的视图状态缓存(损坏/配额满 → 忽略)。
3177
3177
  * @author ddj 2026年08月28号
3178
- * @param sessionId 会话 id
3178
+ * @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
3179
3179
  * @param states path → viewState 映射
3180
3180
  */
3181
- function viewStatesSave(sessionId, states) {
3181
+ function viewStatesSave(scope, states) {
3182
3182
  try {
3183
- window.localStorage.setItem(KEY_PREFIX$2 + String(sessionId), serializeViewStates(states));
3183
+ window.localStorage.setItem(KEY_PREFIX$2 + String(scope), serializeViewStates(states));
3184
3184
  } catch (error) {}
3185
3185
  }
3186
+ //#endregion
3187
+ //#region src/client/state/scopeStore.ts
3188
+ /**
3189
+ * dsh-vscode-mode client — 编辑区状态作用域键(scope key)。
3190
+ * 编辑区 UI 状态(页签/展开树/视图状态/侧边栏等)按「工作区」隔离:同一工作区下
3191
+ * 切换对话恢复同一份状态,无体感差别;会话没有 cwd 时回退按会话隔离(旧行为)。
3192
+ * 另提供旧「会话键」数据到「工作区键」的一次性迁移(只复制不删除,幂等)。
3193
+ * 键前缀统一走 paths.ts PathConst(CACHE_KEY),本模块只产出拼接用的作用域串。
3194
+ * 作者 ddj 2026-09-09
3195
+ */
3196
+ /** Windows 盘符绝对路径(如 D:/ 或 d:\),大小写不敏感。 */
3197
+ const WIN_DRIVE_RE = /^[a-z]:\//i;
3198
+ /**
3199
+ * 归一化工作区路径:trim、反斜杠→斜杠、去尾部斜杠;
3200
+ * Windows 盘符绝对路径整体小写(文件系统大小写不敏感),POSIX 路径保留大小写。
3201
+ * @author ddj 2026年09月09号
3202
+ * @param cwd 工作区目录(sessions 快照 byId[sessionId].cwd)
3203
+ * @returns 归一化路径;空值返回 ''
3204
+ */
3205
+ function normalizeWsPath(cwd) {
3206
+ const s = String(cwd ?? "").trim().replace(/\\/g, "/").replace(/\/+$/, "");
3207
+ if (!s) return "";
3208
+ return WIN_DRIVE_RE.test(s) ? s.toLowerCase() : s;
3209
+ }
3210
+ /**
3211
+ * 计算编辑区状态作用域键:有 cwd → 'ws:' + 归一化路径(同工作区共享);
3212
+ * 无 cwd → 'sid:' + sessionId(回退按会话隔离,对齐旧行为)。
3213
+ * @author ddj 2026年09月09号
3214
+ * @param cwd 工作区目录(可为 null/undefined)
3215
+ * @param sessionId 会话 id(cwd 缺失时的隔离键)
3216
+ * @returns 作用域键字符串
3217
+ */
3218
+ function workspaceScopeOf(cwd, sessionId) {
3219
+ const ws = normalizeWsPath(cwd);
3220
+ if (ws) return "ws:" + ws;
3221
+ return "sid:" + String(sessionId ?? "");
3222
+ }
3223
+ /** 参与迁移的 localStorage 键前缀(编辑区全部按会话持久化的旧键)。 */
3224
+ const MIGRATE_PREFIXES = [
3225
+ CACHE_KEY.editor,
3226
+ CACHE_KEY.viewstate,
3227
+ CACHE_KEY.sidebar + "side.",
3228
+ CACHE_KEY.sidebar,
3229
+ CACHE_KEY.search,
3230
+ CACHE_KEY.expanded,
3231
+ CACHE_KEY.entries
3232
+ ];
3233
+ /** 已迁移过的 scope+sessionId 组合(按 storage 实例分组;每页生命周期只跑一次)。 */
3234
+ const migratedPairs = /* @__PURE__ */ new WeakMap();
3235
+ /**
3236
+ * 把旧「前缀+sessionId」键的数据一次性复制到「前缀+scope」键(目标已存在则跳过)。
3237
+ * 只复制不删除旧键;幂等,可安全重复调用。升级后首个工作区键为空时兜底恢复旧数据。
3238
+ * @author ddj 2026年09月09号
3239
+ * @param scope 目标作用域键
3240
+ * @param sessionId 旧数据所属会话 id
3241
+ * @param storage localStorage 注入口(缺省 window.localStorage,测试可传内存桩)
3242
+ */
3243
+ function migrateScopedKeys(scope, sessionId, storage) {
3244
+ if (!scope || !sessionId) return;
3245
+ let store = null;
3246
+ try {
3247
+ store = storage ?? window.localStorage;
3248
+ } catch (error) {
3249
+ return;
3250
+ }
3251
+ if (!store) return;
3252
+ let pairs = migratedPairs.get(store);
3253
+ if (!pairs) {
3254
+ pairs = /* @__PURE__ */ new Set();
3255
+ migratedPairs.set(store, pairs);
3256
+ }
3257
+ const pair = scope + "|" + sessionId;
3258
+ if (pairs.has(pair)) return;
3259
+ pairs.add(pair);
3260
+ const legacy = String(sessionId);
3261
+ for (const prefix of MIGRATE_PREFIXES) {
3262
+ const target = prefix + scope;
3263
+ try {
3264
+ if (store.getItem(target) !== null) continue;
3265
+ const raw = store.getItem(prefix + legacy);
3266
+ if (raw === null) continue;
3267
+ store.setItem(target, raw);
3268
+ } catch (error) {}
3269
+ }
3270
+ }
3271
+ /** scope →(path → model)缓存表。 */
3272
+ const caches = /* @__PURE__ */ new Map();
3273
+ /** 当前激活作用域(切换时释放上一个)。 */
3274
+ let activeScope = null;
3275
+ /** 模型是否仍附着在某个编辑器实例上(缺方法视为未附着)。 */
3276
+ function isAttached(model) {
3277
+ const fn = model?.isAttachedToEditor;
3278
+ return typeof fn === "function" ? fn.call(model) === true : false;
3279
+ }
3280
+ /** 释放模型(仅未附着编辑器时真正 dispose;附着中的保留给编辑器自毁)。 */
3281
+ function disposeIfFree(model) {
3282
+ if (!model || isAttached(model)) return;
3283
+ try {
3284
+ model.dispose();
3285
+ } catch {}
3286
+ }
3287
+ /**
3288
+ * 取(或切)某作用域的 model 缓存:同作用域重复调用复用同一 Map(跨挂载存活);
3289
+ * 作用域变化时释放上一作用域的模型(未附着的 dispose,附着中的仅移出缓存)。
3290
+ * @author ddj 2026年09月09号
3291
+ * @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
3292
+ * @returns 该作用域的 path → model Map
3293
+ */
3294
+ function modelsForScope(scope) {
3295
+ const existing = caches.get(scope);
3296
+ if (existing && activeScope === scope) return existing;
3297
+ if (activeScope !== null && activeScope !== scope) {
3298
+ const prev = caches.get(activeScope);
3299
+ if (prev) {
3300
+ for (const model of prev.values()) disposeIfFree(model);
3301
+ prev.clear();
3302
+ caches.delete(activeScope);
3303
+ }
3304
+ }
3305
+ activeScope = scope;
3306
+ if (existing) return existing;
3307
+ const map = /* @__PURE__ */ new Map();
3308
+ caches.set(scope, map);
3309
+ return map;
3310
+ }
3311
+ /**
3312
+ * 记入模型并刷新 LRU 序(先删后插保证最新),超上限逐出最旧(未附着才 dispose)。
3313
+ * @author ddj 2026年09月09号
3314
+ * @param map 作用域缓存 Map
3315
+ * @param path 工作区相对路径
3316
+ * @param model Monaco model(或测试桩)
3317
+ */
3318
+ function rememberModel(map, path, model) {
3319
+ if (!path || !model) return;
3320
+ map.delete(path);
3321
+ map.set(path, model);
3322
+ while (map.size > 40) {
3323
+ const oldest = map.keys().next().value;
3324
+ if (oldest === void 0) break;
3325
+ const evicted = map.get(oldest);
3326
+ map.delete(oldest);
3327
+ disposeIfFree(evicted);
3328
+ }
3329
+ }
3186
3330
  let currentMin = 300;
3187
3331
  /**
3188
3332
  * 归一化最小宽度:非法(NaN/非有限/负数)回退默认,越界夹取到 [180, 560]。
@@ -3213,6 +3357,8 @@ window.__ModuleLoader__.load({
3213
3357
  function getSidebarMinWidth() {
3214
3358
  return currentMin;
3215
3359
  }
3360
+ /** scope → 导航历史实例(同工作区跨会话复用同一份历史)。 */
3361
+ const scopeHistories = /* @__PURE__ */ new Map();
3216
3362
  /** 路径一致(容忍相对路径大小写差异,对齐既有 sameFile 语义常用写法)。 */
3217
3363
  function samePath(a, b) {
3218
3364
  if (a === b) return true;
@@ -3276,6 +3422,26 @@ window.__ModuleLoader__.load({
3276
3422
  }
3277
3423
  };
3278
3424
  }
3425
+ /**
3426
+ * 取(或建)某工作区作用域的导航历史实例:同 scope 跨会话复用同一实例,
3427
+ * 后退/前进历史在切换对话后保留(对齐「同一工作区同一编辑区」体感)。
3428
+ * 超出 NAV_SCOPE_CAP 时逐出最旧的实例(Map 插入序近似 LRU)。
3429
+ * @author ddj 2026年09月09号
3430
+ * @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
3431
+ * @param cap 后退栈容量上限(透传 createNavHistory)
3432
+ * @returns 导航历史实例
3433
+ */
3434
+ function navHistoryFor(scope, cap = 200) {
3435
+ const existing = scopeHistories.get(scope);
3436
+ if (existing) return existing;
3437
+ const created = createNavHistory(cap);
3438
+ scopeHistories.set(scope, created);
3439
+ if (scopeHistories.size > 6) {
3440
+ const oldest = scopeHistories.keys().next().value;
3441
+ if (typeof oldest === "string") scopeHistories.delete(oldest);
3442
+ }
3443
+ return created;
3444
+ }
3279
3445
  //#endregion
3280
3446
  //#region src/client/addToConversation.ts
3281
3447
  /** 把追加引用结果映射为可读文案(ok 用 okText;busy 提示已降级纯文本;unavailable 提示不可用)。
@@ -4399,6 +4565,8 @@ window.__ModuleLoader__.load({
4399
4565
  * dsh-vscode-mode client — EditorView:中央 VSCode 式文件编辑器(编排层)。
4400
4566
  * 迁移自原 src/client/index.ts 的 EditorView,语义不改;差异自绘已抽到 monaco/diffRender。
4401
4567
  * 职责:页签/QuickOpen/Monaco 编辑器/差异审查(DiffBox/Launcher/Badge 事件装配)/状态栏/自动保存。
4568
+ * 状态作用域:页签/视图状态/侧边栏/展开树等 UI 状态按工作区作用域(cwd)隔离,
4569
+ * 同一工作区切换对话恢复同一份状态;无 cwd 会话回退按会话隔离(scopeStore)。
4402
4570
  * 作者 ddj 2026-08-20
4403
4571
  */
4404
4572
  /**
@@ -4415,6 +4583,13 @@ window.__ModuleLoader__.load({
4415
4583
  const addToConversation = props.addToConversation;
4416
4584
  const layout = props?.layout === "side" ? "side" : "tab";
4417
4585
  const sideHint = props?.sideHint;
4586
+ const sessionsList = props?.sessions?.list;
4587
+ const cwd = react.default.useSyncExternalStore(react.default.useCallback((onStoreChange) => typeof sessionsList?.subscribe === "function" ? sessionsList.subscribe(onStoreChange) : () => {}, [sessionsList]), () => sessionsList?.getSnapshot?.()?.byId?.[sessionId]?.cwd ?? null, () => null);
4588
+ const scope = react.default.useMemo(() => {
4589
+ const s = workspaceScopeOf(cwd, sessionId);
4590
+ migrateScopedKeys(s, sessionId);
4591
+ return s;
4592
+ }, [cwd, sessionId]);
4418
4593
  const [monaco, setMonaco] = react.default.useState(null);
4419
4594
  const [monacoErr, setMonacoErr] = react.default.useState(null);
4420
4595
  const [records, setRecords] = react.default.useState({});
@@ -4469,15 +4644,16 @@ window.__ModuleLoader__.load({
4469
4644
  const editorRef = react.default.useRef(null);
4470
4645
  const viewRootRef = react.default.useRef(null);
4471
4646
  const monacoRef = react.default.useRef(null);
4472
- const modelsRef = react.default.useRef(/* @__PURE__ */ new Map());
4647
+ const modelsRef = react.default.useRef(null);
4648
+ if (!modelsRef.current) modelsRef.current = modelsForScope(scope);
4473
4649
  const viewStatesRef = react.default.useRef({});
4474
4650
  const saveTimerRef = react.default.useRef(null);
4475
4651
  const loadSeqRef = react.default.useRef(0);
4476
4652
  const programmaticRef = react.default.useRef(false);
4477
- const bootRef = react.default.useRef(false);
4653
+ const restoredScopeRef = react.default.useRef(null);
4478
4654
  const pendingFocusRef = react.default.useRef(null);
4479
4655
  const navRef = react.default.useRef(null);
4480
- if (!navRef.current) navRef.current = createNavHistory();
4656
+ if (!navRef.current) navRef.current = navHistoryFor(scope);
4481
4657
  const navPendingRef = react.default.useRef(null);
4482
4658
  const navCursorTimerRef = react.default.useRef(null);
4483
4659
  const navBackRef = react.default.useRef(null);
@@ -4494,6 +4670,7 @@ window.__ModuleLoader__.load({
4494
4670
  const batchBusyRef = react.default.useRef(false);
4495
4671
  const menuHandlersRef = react.default.useRef(null);
4496
4672
  const doSaveRef = react.default.useRef(null);
4673
+ const saveViewStateRef = react.default.useRef(null);
4497
4674
  const diffRendererRef = react.default.useRef(null);
4498
4675
  const layoutRef = react.default.useRef(layout);
4499
4676
  layoutRef.current = layout;
@@ -4533,7 +4710,7 @@ window.__ModuleLoader__.load({
4533
4710
  if (select) setActive(path);
4534
4711
  };
4535
4712
  /**
4536
- * 保存当前活动文件的视图状态(光标/滚动/折叠)到会话缓存。
4713
+ * 保存当前活动文件的视图状态(光标/滚动/折叠)到工作区作用域缓存。
4537
4714
  * @author ddj 2026年08月28号
4538
4715
  * @param path 要保存的文件路径(缺省 = 当前 active)
4539
4716
  */
@@ -4545,9 +4722,10 @@ window.__ModuleLoader__.load({
4545
4722
  const state = ed.saveViewState();
4546
4723
  if (!state) return;
4547
4724
  viewStatesRef.current = upsertViewState(viewStatesRef.current, target, state);
4548
- viewStatesSave(sessionId, viewStatesRef.current);
4725
+ viewStatesSave(scope, viewStatesRef.current);
4549
4726
  } catch (e) {}
4550
4727
  };
4728
+ saveViewStateRef.current = saveViewState;
4551
4729
  /**
4552
4730
  * 恢复指定文件的视图状态(内容就绪、setModel 后调用;恢复后即消费删除)。
4553
4731
  * @author ddj 2026年08月28号
@@ -5038,11 +5216,12 @@ window.__ModuleLoader__.load({
5038
5216
  };
5039
5217
  }, [sessionId, layout]);
5040
5218
  react.default.useEffect(() => {
5041
- if (bootRef.current || !sessionId) return;
5042
- bootRef.current = true;
5043
- viewStatesRef.current = viewStatesLoad(sessionId);
5219
+ if (!sessionId || restoredScopeRef.current === scope) return;
5220
+ restoredScopeRef.current = scope;
5221
+ modelsRef.current = modelsForScope(scope);
5222
+ viewStatesRef.current = viewStatesLoad(scope);
5044
5223
  try {
5045
- const raw = localStorage.getItem(CACHE_KEY.editor + String(sessionId));
5224
+ const raw = localStorage.getItem(CACHE_KEY.editor + String(scope));
5046
5225
  if (raw) {
5047
5226
  const saved = JSON.parse(raw);
5048
5227
  if (Array.isArray(saved.tabs) && saved.tabs.length) {
@@ -5051,20 +5230,20 @@ window.__ModuleLoader__.load({
5051
5230
  }
5052
5231
  }
5053
5232
  } catch (e) {}
5054
- }, [sessionId]);
5233
+ }, [sessionId, scope]);
5055
5234
  react.default.useEffect(() => {
5056
- navRef.current = createNavHistory();
5235
+ navRef.current = navHistoryFor(scope);
5057
5236
  navPendingRef.current = null;
5058
5237
  if (navCursorTimerRef.current) {
5059
5238
  clearTimeout(navCursorTimerRef.current);
5060
5239
  navCursorTimerRef.current = null;
5061
5240
  }
5062
5241
  setNavTick((v) => v + 1);
5063
- }, [sessionId]);
5242
+ }, [scope]);
5064
5243
  react.default.useEffect(() => {
5065
5244
  if (!sessionId) return;
5066
5245
  try {
5067
- localStorage.setItem(CACHE_KEY.editor + String(sessionId), JSON.stringify({
5246
+ localStorage.setItem(CACHE_KEY.editor + String(scope), JSON.stringify({
5068
5247
  tabs: tabs.map((t) => t.path),
5069
5248
  active
5070
5249
  }));
@@ -5072,9 +5251,10 @@ window.__ModuleLoader__.load({
5072
5251
  }, [
5073
5252
  tabs,
5074
5253
  active,
5075
- sessionId
5254
+ sessionId,
5255
+ scope
5076
5256
  ]);
5077
- const sidebarKey = CACHE_KEY.sidebar + (layout === "side" ? "side." : "") + String(sessionId);
5257
+ const sidebarKey = CACHE_KEY.sidebar + (layout === "side" ? "side." : "") + String(scope);
5078
5258
  react.default.useEffect(() => {
5079
5259
  if (!sessionId) return;
5080
5260
  try {
@@ -5259,8 +5439,9 @@ window.__ModuleLoader__.load({
5259
5439
  const cache = modelsRef.current;
5260
5440
  let model = cache.get(path);
5261
5441
  if (!model) {
5262
- model = window.monaco.editor.createModel(text ?? "", langOf(path), window.monaco.Uri.parse("edrv:///" + encodeURI(path)));
5263
- cache.set(path, model);
5442
+ const uri = window.monaco.Uri.parse("edrv:///" + encodeURI(path));
5443
+ model = window.monaco.editor.getModel(uri) || window.monaco.editor.createModel(text ?? "", langOf(path), uri);
5444
+ rememberModel(cache, path, model);
5264
5445
  } else if (text !== void 0 && model.getValue() !== text) {
5265
5446
  programmaticRef.current = true;
5266
5447
  model.setValue(text);
@@ -5382,14 +5563,13 @@ window.__ModuleLoader__.load({
5382
5563
  ]);
5383
5564
  react.default.useEffect(() => () => {
5384
5565
  flushSave();
5385
- saveViewState(active);
5566
+ flushNavCursor();
5567
+ saveViewStateRef.current?.();
5386
5568
  diffRendererRef.current?.dispose?.();
5387
5569
  if (editorRef.current) {
5388
5570
  editorRef.current.dispose();
5389
5571
  editorRef.current = null;
5390
5572
  }
5391
- for (const m of modelsRef.current.values()) m.dispose();
5392
- modelsRef.current.clear();
5393
5573
  for (const ctl of pdfCtlRef.current.values()) ctl.destroy();
5394
5574
  pdfCtlRef.current.clear();
5395
5575
  const root = editorRef.current && editorRef.current.getDomNode ? editorRef.current.getDomNode() : null;
@@ -6317,7 +6497,8 @@ window.__ModuleLoader__.load({
6317
6497
  const sidebarPanels = props.sidebarPanels;
6318
6498
  const sidebarCtx = {
6319
6499
  sessionId,
6320
- cwd: props.sessions?.list?.getSnapshot?.()?.byId?.[sessionId]?.cwd ?? null,
6500
+ cwd: cwd ?? null,
6501
+ scope,
6321
6502
  openFile: (p) => openFile(p, false),
6322
6503
  openFileAt,
6323
6504
  activePath: active,
@@ -7120,6 +7301,59 @@ window.__ModuleLoader__.load({
7120
7301
  if (typeof bodyDisposer === "function") bodyDisposer();
7121
7302
  };
7122
7303
  }
7304
+ /** 编辑 Tab 是否处于激活态(正文挂载 = 激活;模块级,跨会话存活)。 */
7305
+ let editorTabActive = false;
7306
+ /** 正文挂载纪元(每次挂载自增;延迟判定期间有新挂载则放弃清除)。 */
7307
+ let mountEpoch = 0;
7308
+ /**
7309
+ * 标记编辑 Tab 激活并推进挂载纪元(正文挂载时调用)。
7310
+ * @author ddj 2026年09月09号
7311
+ */
7312
+ function markEditorMounted() {
7313
+ mountEpoch += 1;
7314
+ markEditorActive$1(true);
7315
+ }
7316
+ /**
7317
+ * 直接设置激活标记(正文卸载延迟判定用;测试 also 用作状态编排入口)。
7318
+ * @author ddj 2026年09月09号
7319
+ * @param active 是否激活
7320
+ */
7321
+ function markEditorActive$1(active) {
7322
+ editorTabActive = active;
7323
+ }
7324
+ /**
7325
+ * 读编辑 Tab 激活标记(会话切换时决定是否自动恢复编辑栏)。
7326
+ * @author ddj 2026年09月09号
7327
+ * @returns 上次标记的激活状态
7328
+ */
7329
+ function isEditorTabActive() {
7330
+ return editorTabActive;
7331
+ }
7332
+ /**
7333
+ * 读当前挂载纪元(卸载延迟判定用:纪元已推进 = 有新正文接管,不清标记)。
7334
+ * @author ddj 2026年09月09号
7335
+ * @returns 当前纪元
7336
+ */
7337
+ function editorMountEpoch() {
7338
+ return mountEpoch;
7339
+ }
7340
+ /**
7341
+ * 在挂载会话恢复编辑 Tab:已激活跳过(官方幂等);seat 未就绪抛错时有界重试。
7342
+ * openTab 官方语义「打开即展开侧栏」,一次调用同时完成展开 + 激活。
7343
+ * @author ddj 2026年09月09号
7344
+ * @param service 官方 sidebarRight 服务(缺失/未装配直接返回)
7345
+ * @param schedule 延时调度(ctx.timeout;重试节拍)
7346
+ * @param attempts 剩余尝试次数(默认 10 次 × 150ms ≈ 1.5s 窗口)
7347
+ */
7348
+ function restoreEditorTab(service, schedule, attempts = 10) {
7349
+ if (!service || typeof service.openTab !== "function" || !editorTabActive) return;
7350
+ try {
7351
+ if (service.active?.()?.kind === "edrvEditor") return;
7352
+ service.openTab(OFFICIAL_TAB_KIND, {});
7353
+ } catch {
7354
+ if (attempts > 1) schedule(() => restoreEditorTab(service, schedule, attempts - 1), 150);
7355
+ }
7356
+ }
7123
7357
  //#endregion
7124
7358
  //#region src/client/fileOpeners.ts
7125
7359
  /**
@@ -9874,9 +10108,24 @@ window.__ModuleLoader__.load({
9874
10108
  }
9875
10109
  const revision = typeof navigation?.revision === "number" ? navigation.revision : -1;
9876
10110
  const revisionRef = react.default.useRef(-1);
10111
+ const sessionIdRef = react.default.useRef(sessionId);
10112
+ sessionIdRef.current = sessionId;
9877
10113
  react.default.useEffect(() => {
9878
10114
  setSideEditorMounted(true);
9879
- return () => setSideEditorMounted(false);
10115
+ markEditorMounted();
10116
+ return () => {
10117
+ setSideEditorMounted(false);
10118
+ const epoch = editorMountEpoch();
10119
+ const self = sessionIdRef.current;
10120
+ const schedule = props?.schedule;
10121
+ const decide = () => {
10122
+ if (editorMountEpoch() !== epoch) return;
10123
+ const current = props?.sessions?.list?.getSnapshot?.()?.current;
10124
+ markEditorActive(current === self);
10125
+ };
10126
+ if (typeof schedule === "function") schedule(decide, 0);
10127
+ else decide();
10128
+ };
9880
10129
  }, []);
9881
10130
  react.default.useEffect(() => {
9882
10131
  if (revision === revisionRef.current) return;
@@ -10103,7 +10352,7 @@ window.__ModuleLoader__.load({
10103
10352
  * 键前缀统一走 paths.ts PathConst(CACHE_KEY.expanded)。
10104
10353
  * 作者 ddj 2026-08-28
10105
10354
  */
10106
- /** localStorage 键前缀(按会话隔离)。 */
10355
+ /** localStorage 键前缀(按工作区作用域隔离,无 cwd 会话回退会话键)。 */
10107
10356
  const KEY_PREFIX$1 = CACHE_KEY.expanded;
10108
10357
  /**
10109
10358
  * 归一化展开路径:去空白、反斜杠→斜杠、去首尾斜杠,`..` 段拒绝。
@@ -10175,27 +10424,27 @@ window.__ModuleLoader__.load({
10175
10424
  }
10176
10425
  }
10177
10426
  /**
10178
- * 读取某会话的展开状态缓存(localStorage 不可用 → null)。
10427
+ * 读取某作用域的展开状态缓存(localStorage 不可用 → null)。
10179
10428
  * @author ddj 2026年08月28号
10180
- * @param sessionId 会话 id
10429
+ * @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
10181
10430
  * @returns 缓存数据或 null
10182
10431
  */
10183
- function explorerLoad(sessionId) {
10432
+ function explorerLoad(scope) {
10184
10433
  try {
10185
- return parseExplorer(window.localStorage.getItem(KEY_PREFIX$1 + String(sessionId)));
10434
+ return parseExplorer(window.localStorage.getItem(KEY_PREFIX$1 + String(scope)));
10186
10435
  } catch (error) {
10187
10436
  return null;
10188
10437
  }
10189
10438
  }
10190
10439
  /**
10191
- * 写入某会话的展开状态缓存(损坏/配额满 → 忽略)。
10440
+ * 写入某作用域的展开状态缓存(损坏/配额满 → 忽略)。
10192
10441
  * @author ddj 2026年08月28号
10193
- * @param sessionId 会话 id
10442
+ * @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
10194
10443
  * @param data 缓存数据
10195
10444
  */
10196
- function explorerSave(sessionId, data) {
10445
+ function explorerSave(scope, data) {
10197
10446
  try {
10198
- window.localStorage.setItem(KEY_PREFIX$1 + String(sessionId), serializeExplorer(data));
10447
+ window.localStorage.setItem(KEY_PREFIX$1 + String(scope), serializeExplorer(data));
10199
10448
  } catch (error) {}
10200
10449
  }
10201
10450
  //#endregion
@@ -10260,9 +10509,9 @@ window.__ModuleLoader__.load({
10260
10509
  return null;
10261
10510
  }
10262
10511
  }
10263
- /** sessionId → 缓存镜像(同步读,避免每次渲染 parse)。 */
10512
+ /** scope → 缓存镜像(同步读,避免每次渲染 parse)。 */
10264
10513
  const mirrors = /* @__PURE__ */ new Map();
10265
- /** sessionId → 落盘防抖计时器。 */
10514
+ /** scope → 落盘防抖计时器。 */
10266
10515
  const saveTimers = /* @__PURE__ */ new Map();
10267
10516
  /** 单调时钟:连续 put 的 ts 严格递增(同毫秒不并列,逐出顺序可预期)。 */
10268
10517
  let clock = 0;
@@ -10272,82 +10521,82 @@ window.__ModuleLoader__.load({
10272
10521
  return clock;
10273
10522
  }
10274
10523
  /** localStorage 读取(不可用 → null)。 */
10275
- function readRaw(sessionId) {
10524
+ function readRaw(scope) {
10276
10525
  try {
10277
- return window.localStorage.getItem(KEY_PREFIX + sessionId);
10526
+ return window.localStorage.getItem(KEY_PREFIX + scope);
10278
10527
  } catch (error) {
10279
10528
  return null;
10280
10529
  }
10281
10530
  }
10282
10531
  /** localStorage 写入(配额/隐私模式失败静默)。 */
10283
- function writeRaw(sessionId, text) {
10532
+ function writeRaw(scope, text) {
10284
10533
  try {
10285
- window.localStorage.setItem(KEY_PREFIX + sessionId, text);
10534
+ window.localStorage.setItem(KEY_PREFIX + scope, text);
10286
10535
  } catch (error) {}
10287
10536
  }
10288
- /** 取(或建)会话缓存镜像。 */
10289
- function mirrorOf(sessionId) {
10290
- const existing = mirrors.get(sessionId);
10537
+ /** 取(或建)作用域缓存镜像。 */
10538
+ function mirrorOf(scope) {
10539
+ const existing = mirrors.get(scope);
10291
10540
  if (existing) return existing;
10292
- const data = entriesParse(readRaw(sessionId)) ?? {
10541
+ const data = entriesParse(readRaw(scope)) ?? {
10293
10542
  v: 2,
10294
10543
  root: null,
10295
10544
  dirs: {}
10296
10545
  };
10297
- mirrors.set(sessionId, data);
10546
+ mirrors.set(scope, data);
10298
10547
  return data;
10299
10548
  }
10300
10549
  /** 调度防抖落盘(重置已有计时器)。 */
10301
- function scheduleSave(sessionId) {
10302
- const prev = saveTimers.get(sessionId);
10550
+ function scheduleSave(scope) {
10551
+ const prev = saveTimers.get(scope);
10303
10552
  if (prev) clearTimeout(prev);
10304
- saveTimers.set(sessionId, setTimeout(() => {
10305
- saveTimers.delete(sessionId);
10306
- const data = mirrors.get(sessionId);
10307
- if (data) writeRaw(sessionId, entriesSerialize(entriesTrim(data)));
10553
+ saveTimers.set(scope, setTimeout(() => {
10554
+ saveTimers.delete(scope);
10555
+ const data = mirrors.get(scope);
10556
+ if (data) writeRaw(scope, entriesSerialize(entriesTrim(data)));
10308
10557
  }, 300));
10309
10558
  }
10310
10559
  /**
10311
10560
  * 读某目录缓存条目(无 → null;展示不受新鲜度限制)。
10312
10561
  * @author ddj 2026年08月31号
10313
- * @param sessionId 会话 id
10562
+ * @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
10314
10563
  * @param rel 目录相对路径('' = 根)
10315
10564
  * @returns 缓存条目或 null
10316
10565
  */
10317
- function entriesCacheGet(sessionId, rel) {
10318
- if (!sessionId) return null;
10319
- const dir = mirrorOf(sessionId).dirs[rel];
10566
+ function entriesCacheGet(scope, rel) {
10567
+ if (!scope) return null;
10568
+ const dir = mirrorOf(scope).dirs[rel];
10320
10569
  if (!dir || !dir.entries.length) return null;
10321
10570
  return dir.entries;
10322
10571
  }
10323
10572
  /**
10324
10573
  * 某目录缓存是否新鲜(预取去重用)。
10325
10574
  * @author ddj 2026年08月31号
10326
- * @param sessionId 会话 id
10575
+ * @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
10327
10576
  * @param rel 目录相对路径
10328
10577
  * @returns 新鲜返回 true
10329
10578
  */
10330
- function entriesCacheIsFresh(sessionId, rel) {
10331
- if (!sessionId) return false;
10332
- const dir = mirrorOf(sessionId).dirs[rel];
10579
+ function entriesCacheIsFresh(scope, rel) {
10580
+ if (!scope) return false;
10581
+ const dir = mirrorOf(scope).dirs[rel];
10333
10582
  return Boolean(dir && Date.now() - dir.ts < 3e4);
10334
10583
  }
10335
10584
  /**
10336
10585
  * 写入某目录缓存条目(内存镜像 + 防抖落盘;单目录条目截断)。
10337
10586
  * @author ddj 2026年08月31号
10338
- * @param sessionId 会话 id
10587
+ * @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
10339
10588
  * @param rel 目录相对路径
10340
10589
  * @param entries 目录条目
10341
10590
  */
10342
- function entriesCachePut(sessionId, rel, entries) {
10343
- if (!sessionId) return;
10344
- const data = mirrorOf(sessionId);
10591
+ function entriesCachePut(scope, rel, entries) {
10592
+ if (!scope) return;
10593
+ const data = mirrorOf(scope);
10345
10594
  data.dirs[rel] = {
10346
10595
  ts: nextTs(),
10347
10596
  entries: entries.slice(0, 500)
10348
10597
  };
10349
10598
  data.dirs = entriesTrim(data).dirs;
10350
- scheduleSave(sessionId);
10599
+ scheduleSave(scope);
10351
10600
  }
10352
10601
  //#endregion
10353
10602
  //#region src/client/sidebar/panels/FileExplorer.ts
@@ -10379,6 +10628,7 @@ window.__ModuleLoader__.load({
10379
10628
  function FileExplorer(props) {
10380
10629
  const ctx = props?.ctx;
10381
10630
  const sessionId = ctx?.sessionId;
10631
+ const scope = ctx?.scope ?? workspaceScopeOf(null, sessionId);
10382
10632
  const openFile = ctx?.openFile;
10383
10633
  const activePath = ctx?.activePath ?? null;
10384
10634
  const pendingByPath = ctx?.pendingByPath ?? {};
@@ -10399,14 +10649,14 @@ window.__ModuleLoader__.load({
10399
10649
  const loadDirRef = react.default.useRef(null);
10400
10650
  const refreshRef = react.default.useRef(null);
10401
10651
  /** 渲染取数:内存态 → 本地条目缓存 → null(显示加载态)。 */
10402
- const entriesOf = (rel) => dirsRef.current[rel] ?? entriesCacheGet(sessionId, rel) ?? null;
10652
+ const entriesOf = (rel) => dirsRef.current[rel] ?? entriesCacheGet(scope, rel) ?? null;
10403
10653
  /** 预取子目录:≤4 个、排除重型目录、缓存新鲜跳过、已加载跳过、不级联。 */
10404
10654
  const prefetchDirs = (rel, entries) => {
10405
10655
  const want = [];
10406
10656
  for (const e of entries) {
10407
10657
  if (e.type !== "directory") continue;
10408
10658
  if (PREFETCH_EXCLUDED.has(e.name)) continue;
10409
- if (entriesCacheIsFresh(sessionId, e.path)) continue;
10659
+ if (entriesCacheIsFresh(scope, e.path)) continue;
10410
10660
  if (dirsRef.current[e.path] !== void 0) continue;
10411
10661
  want.push(e.path);
10412
10662
  if (want.length >= PREFETCH_MAX) break;
@@ -10439,7 +10689,7 @@ window.__ModuleLoader__.load({
10439
10689
  if (res && res.ok && Array.isArray(res.entries)) {
10440
10690
  setDirs((prev) => Object.assign({}, prev, { [rel]: res.entries }));
10441
10691
  if (res.root) setRoot((prev) => prev || res.root);
10442
- entriesCachePut(sessionId, rel, res.entries);
10692
+ entriesCachePut(scope, rel, res.entries);
10443
10693
  if (doPrefetch) prefetchDirs(rel, res.entries);
10444
10694
  } else if (!entriesOf(rel)) setErrors((prev) => Object.assign({}, prev, { [rel]: res?.error ? String(res.error) : "读取目录失败" }));
10445
10695
  }).catch((e) => {
@@ -10492,7 +10742,7 @@ window.__ModuleLoader__.load({
10492
10742
  setErrors({});
10493
10743
  setMenu(null);
10494
10744
  setRoot(null);
10495
- const cached = sessionId ? explorerLoad(sessionId) : null;
10745
+ const cached = scope ? explorerLoad(scope) : null;
10496
10746
  const restored = cached?.expanded ?? [];
10497
10747
  if (cached?.root) setRoot(cached.root);
10498
10748
  if (restored.length) {
@@ -10502,17 +10752,17 @@ window.__ModuleLoader__.load({
10502
10752
  }
10503
10753
  loadDir("", { prefetch: true });
10504
10754
  for (const rel of restored) if (rel) loadDir(rel, { prefetch: true });
10505
- }, [sessionId]);
10755
+ }, [scope]);
10506
10756
  react.default.useEffect(() => {
10507
10757
  if (saveTimerRef.current) {
10508
10758
  clearTimeout(saveTimerRef.current);
10509
10759
  saveTimerRef.current = null;
10510
10760
  }
10511
- if (!sessionId) return;
10761
+ if (!scope) return;
10512
10762
  saveTimerRef.current = setTimeout(() => {
10513
10763
  saveTimerRef.current = null;
10514
10764
  const expandedList = Object.keys(expanded).filter((k) => expanded[k] === true);
10515
- explorerSave(sessionId, {
10765
+ explorerSave(scope, {
10516
10766
  root,
10517
10767
  expanded: expandedList
10518
10768
  });
@@ -10526,7 +10776,7 @@ window.__ModuleLoader__.load({
10526
10776
  }, [
10527
10777
  expanded,
10528
10778
  root,
10529
- sessionId
10779
+ scope
10530
10780
  ]);
10531
10781
  react.default.useEffect(() => {
10532
10782
  const onRefresh = () => refreshRef.current?.();
@@ -10534,14 +10784,14 @@ window.__ModuleLoader__.load({
10534
10784
  return () => window.removeEventListener("edrv:refresh", onRefresh);
10535
10785
  }, []);
10536
10786
  react.default.useEffect(() => {
10537
- if (!sessionId) return;
10787
+ if (!scope) return;
10538
10788
  const timer = setInterval(() => {
10539
10789
  const open = Object.keys(expandedRef.current).filter((k) => expandedRef.current[k] === true);
10540
10790
  if (!open.length) return;
10541
10791
  for (const rel of open) loadDirRef.current?.(rel, {});
10542
10792
  }, FOLLOW_INTERVAL_MS);
10543
10793
  return () => clearInterval(timer);
10544
- }, [sessionId]);
10794
+ }, [scope]);
10545
10795
  const rootName = root ? String(root).split(/[\\/]/).pop() || root : "";
10546
10796
  const rowEl = (e, depth, isDir, isOpen) => {
10547
10797
  const pending = isDir ? 0 : pendingByPath[e.path] ?? 0;
@@ -10680,6 +10930,7 @@ window.__ModuleLoader__.load({
10680
10930
  function SearchPanel(props) {
10681
10931
  const ctx = props?.ctx;
10682
10932
  const sessionId = ctx?.sessionId;
10933
+ const scope = ctx?.scope ?? workspaceScopeOf(null, sessionId);
10683
10934
  const openFileAt = ctx?.openFileAt ?? (() => {});
10684
10935
  const activePath = ctx?.activePath ?? null;
10685
10936
  const [query, setQuery] = react.default.useState("");
@@ -10719,9 +10970,9 @@ window.__ModuleLoader__.load({
10719
10970
  exclude: excludeOn ? splitGlobs(excludeText) : []
10720
10971
  };
10721
10972
  react.default.useEffect(() => {
10722
- if (!sessionId) return;
10973
+ if (!scope) return;
10723
10974
  try {
10724
- const saved = JSON.parse(localStorage.getItem(CACHE_KEY.search + String(sessionId)) || "null");
10975
+ const saved = JSON.parse(localStorage.getItem(CACHE_KEY.search + String(scope)) || "null");
10725
10976
  if (saved && typeof saved === "object") {
10726
10977
  if (typeof saved.query === "string") setQuery(saved.query);
10727
10978
  if (typeof saved.matchCase === "boolean") setMatchCase(saved.matchCase);
@@ -10734,11 +10985,11 @@ window.__ModuleLoader__.load({
10734
10985
  if (typeof saved.sectionOpen === "boolean") setSectionOpen(saved.sectionOpen);
10735
10986
  }
10736
10987
  } catch (e) {}
10737
- }, [sessionId]);
10988
+ }, [scope]);
10738
10989
  react.default.useEffect(() => {
10739
- if (!sessionId) return;
10990
+ if (!scope) return;
10740
10991
  try {
10741
- localStorage.setItem(CACHE_KEY.search + String(sessionId), JSON.stringify({
10992
+ localStorage.setItem(CACHE_KEY.search + String(scope), JSON.stringify({
10742
10993
  query,
10743
10994
  matchCase,
10744
10995
  wholeWord,
@@ -10751,7 +11002,7 @@ window.__ModuleLoader__.load({
10751
11002
  }));
10752
11003
  } catch (e) {}
10753
11004
  }, [
10754
- sessionId,
11005
+ scope,
10755
11006
  query,
10756
11007
  matchCase,
10757
11008
  wholeWord,
@@ -12622,6 +12873,23 @@ window.__ModuleLoader__.load({
12622
12873
  setSession(list.getSnapshot()?.current);
12623
12874
  }), "vscode-mode: lsp session sync");
12624
12875
  }
12876
+ if (monacoList && typeof monacoList.subscribe === "function") {
12877
+ const list = monacoList;
12878
+ ctx.effect(() => {
12879
+ let lastCurrent = list.getSnapshot?.()?.current;
12880
+ const check = () => {
12881
+ const current = list.getSnapshot?.()?.current;
12882
+ if (!current || current === lastCurrent) return;
12883
+ lastCurrent = current;
12884
+ if (!isEditorTabActive() || officialService === void 0) return;
12885
+ try {
12886
+ officialService.service.openTabIn?.(current, OFFICIAL_TAB_KIND, {});
12887
+ } catch {}
12888
+ restoreEditorTab(officialService.service, schedule);
12889
+ };
12890
+ return list.subscribe(() => schedule(check, 0));
12891
+ }, "vscode-mode: editor tab restore");
12892
+ }
12625
12893
  const originalOpenPath = workspaces?.openPath;
12626
12894
  const binder = pickSettingsBinder(ctx);
12627
12895
  const settings = binder.scope;