dsh-vscode-mode 0.1.59 → 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/README.md +21 -11
- package/lib/client.js +800 -107
- package/lib/client.js.map +1 -1
- package/package.json +2 -2
- package/src/client/fileOpeners.ts +41 -1
- package/src/client/index.ts +109 -23
- package/src/client/navHistory.ts +28 -1
- package/src/client/officialSidebar.ts +352 -0
- 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 +58 -27
- package/src/client/ui/OfficialSideTab.ts +92 -0
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
|
-
*
|
|
3163
|
+
* 读取某作用域的视图状态缓存(localStorage 不可用 → 空映射)。
|
|
3164
3164
|
* @author ddj 2026年08月28号
|
|
3165
|
-
* @param
|
|
3165
|
+
* @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
|
|
3166
3166
|
* @returns path → viewState 映射
|
|
3167
3167
|
*/
|
|
3168
|
-
function viewStatesLoad(
|
|
3168
|
+
function viewStatesLoad(scope) {
|
|
3169
3169
|
try {
|
|
3170
|
-
return parseViewStates(window.localStorage.getItem(KEY_PREFIX$2 + String(
|
|
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
|
|
3178
|
+
* @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
|
|
3179
3179
|
* @param states path → viewState 映射
|
|
3180
3180
|
*/
|
|
3181
|
-
function viewStatesSave(
|
|
3181
|
+
function viewStatesSave(scope, states) {
|
|
3182
3182
|
try {
|
|
3183
|
-
window.localStorage.setItem(KEY_PREFIX$2 + String(
|
|
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(
|
|
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
|
|
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 =
|
|
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(
|
|
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 (
|
|
5042
|
-
|
|
5043
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
}, [
|
|
5242
|
+
}, [scope]);
|
|
5064
5243
|
react.default.useEffect(() => {
|
|
5065
5244
|
if (!sessionId) return;
|
|
5066
5245
|
try {
|
|
5067
|
-
localStorage.setItem(CACHE_KEY.editor + String(
|
|
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(
|
|
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
|
-
|
|
5263
|
-
|
|
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
|
-
|
|
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:
|
|
6500
|
+
cwd: cwd ?? null,
|
|
6501
|
+
scope,
|
|
6321
6502
|
openFile: (p) => openFile(p, false),
|
|
6322
6503
|
openFileAt,
|
|
6323
6504
|
activePath: active,
|
|
@@ -6343,7 +6524,7 @@ window.__ModuleLoader__.load({
|
|
|
6343
6524
|
const copyInstallCmd = () => {
|
|
6344
6525
|
if (navigator.clipboard?.writeText) navigator.clipboard.writeText(SIDEBAR_INSTALL_CMD).catch(() => {});
|
|
6345
6526
|
};
|
|
6346
|
-
const sideHintEl = layout === "tab" && sideHint && !hintDismissed ? react.default.createElement("div", { className: "edrv-side-hint" }, react.default.createElement("span", { className: "edrv-side-hint-text" }, "
|
|
6527
|
+
const sideHintEl = layout === "tab" && sideHint && !hintDismissed ? react.default.createElement("div", { className: "edrv-side-hint" }, react.default.createElement("span", { className: "edrv-side-hint-text" }, "DSH 0.1.5+ 自带官方右侧侧边栏(升级即用);旧版可安装 dsh-better-sidebar 启用侧边栏编辑(对话+编辑同屏):"), react.default.createElement("code", { className: "edrv-side-hint-cmd" }, SIDEBAR_INSTALL_CMD), react.default.createElement("button", {
|
|
6347
6528
|
className: "edrv-pill edrv-pill-ghost",
|
|
6348
6529
|
title: "复制安装命令",
|
|
6349
6530
|
onClick: copyInstallCmd
|
|
@@ -6883,14 +7064,307 @@ window.__ModuleLoader__.load({
|
|
|
6883
7064
|
};
|
|
6884
7065
|
}
|
|
6885
7066
|
//#endregion
|
|
7067
|
+
//#region src/client/officialSidebar.ts
|
|
7068
|
+
/**
|
|
7069
|
+
* dsh-vscode-mode client — 官方右侧 Sidebar 桥:探测 DSH 0.1.5-alpha.1+ 的
|
|
7070
|
+
* ctx.sidebarRightTabs / ctx.sidebarRight 服务,两段式注册「文件编辑」Tab
|
|
7071
|
+
* (类型定义进注册表,正文进 keyed slot sidebar.right.pane.tab)。
|
|
7072
|
+
* 可选依赖模式:服务缺失时调用方回退 better-sidebar / 中央页签;本模块不注册任何东西。
|
|
7073
|
+
* 维护约定:官方侧边栏(DSH 0.1.5+)是侧边栏编辑形态的唯一维护面;
|
|
7074
|
+
* dsh-better-sidebar 桥(sidebarBridge.ts)已归档,仅作旧版 DSH 回退,不再新增能力。
|
|
7075
|
+
* 纯逻辑(探测/导航参数解析)可单测,注册装配仅存在于 install 路径。
|
|
7076
|
+
* 作者 ddj 2026年09月09号
|
|
7077
|
+
*/
|
|
7078
|
+
/** 官方 Tab 类型注册表服务名(ctx.get 用;不进 inject——旧版 DSH 无此包,inject 会停等)。 */
|
|
7079
|
+
const OFFICIAL_TABS_SERVICE = "sidebarRightTabs";
|
|
7080
|
+
/** 官方导航控制器服务名(ctx.get 用)。 */
|
|
7081
|
+
const OFFICIAL_SERVICE = "sidebarRight";
|
|
7082
|
+
/** 官方 keyed slot 名(Tab 正文挂点,由官方侧边栏的 rightbar seat 声明)。 */
|
|
7083
|
+
const OFFICIAL_SLOT_NAME = "sidebar.right.pane.tab";
|
|
7084
|
+
/** 本插件 Tab 类型定义 id(官方约定用包名;正文 slot 的 key 与之相同)。 */
|
|
7085
|
+
const OFFICIAL_TAB_ID = "dsh-vscode-mode";
|
|
7086
|
+
/** 本插件 Tab 的类型判别符(ctx.sidebarRight.openTab 的 kind)。 */
|
|
7087
|
+
const OFFICIAL_TAB_KIND = "edrvEditor";
|
|
7088
|
+
/** Tab 标题(与 better-sidebar 形态一致)。 */
|
|
7089
|
+
const OFFICIAL_TAB_TITLE = "文件编辑";
|
|
7090
|
+
/** file 地址认领的 patterns(extension 档接管官方 textpreview 的 fallback 认领,注销自动回落)。 */
|
|
7091
|
+
const OFFICIAL_FILE_PATTERN = "dsh-resource://file/**";
|
|
7092
|
+
/** file 资源类型的定义 id(正文 slot 的 key)。 */
|
|
7093
|
+
const OFFICIAL_FILE_TAB_ID = "dsh-vscode-mode/file";
|
|
7094
|
+
/** file 资源类型的 kind(与页类型 edrvEditor 区分:同 kind 只允许一份 extension)。 */
|
|
7095
|
+
const OFFICIAL_FILE_KIND = "edrvEditorFile";
|
|
7096
|
+
/**
|
|
7097
|
+
* 结构化探测官方右侧 Sidebar 服务(可选依赖:缺失/降级返回 undefined)。
|
|
7098
|
+
* 服务仅存在于 DSH 0.1.5-alpha.1+,探测命中 ≡ 版本判定成立。
|
|
7099
|
+
* @author ddj 2026年09月09号
|
|
7100
|
+
* @param ctx 客户端服务上下文
|
|
7101
|
+
* @returns 注册表与控制器对,或 undefined
|
|
7102
|
+
*/
|
|
7103
|
+
function detectOfficial(ctx) {
|
|
7104
|
+
try {
|
|
7105
|
+
const tabs = ctx.get(OFFICIAL_TABS_SERVICE);
|
|
7106
|
+
const service = ctx.get(OFFICIAL_SERVICE);
|
|
7107
|
+
if (!tabs || typeof tabs.register !== "function") return void 0;
|
|
7108
|
+
if (!service || typeof service.openTab !== "function") return void 0;
|
|
7109
|
+
return {
|
|
7110
|
+
tabs,
|
|
7111
|
+
service
|
|
7112
|
+
};
|
|
7113
|
+
} catch {
|
|
7114
|
+
return;
|
|
7115
|
+
}
|
|
7116
|
+
}
|
|
7117
|
+
/**
|
|
7118
|
+
* 从官方导航参数安全解析打开请求(缺字段/坏类型一律降级为空请求,不抛错)。
|
|
7119
|
+
* @author ddj 2026年09月09号
|
|
7120
|
+
* @param params openTab 传入的 params(正文经 navigation.params 读回)
|
|
7121
|
+
* @returns 待打开路径与是否聚焦首个差异
|
|
7122
|
+
*/
|
|
7123
|
+
function resolveNavOpen(params) {
|
|
7124
|
+
const value = params ?? {};
|
|
7125
|
+
return {
|
|
7126
|
+
path: typeof value?.openPath === "string" && value.openPath ? value.openPath : null,
|
|
7127
|
+
focusDiff: value?.focusDiff === true
|
|
7128
|
+
};
|
|
7129
|
+
}
|
|
7130
|
+
/** 逐段 component 解码(失败返回 null,对齐官方 parseFileAddress 的容错语义)。 */
|
|
7131
|
+
function decodeSegment(raw) {
|
|
7132
|
+
if (raw === void 0) return null;
|
|
7133
|
+
try {
|
|
7134
|
+
return decodeURIComponent(raw);
|
|
7135
|
+
} catch {
|
|
7136
|
+
return null;
|
|
7137
|
+
}
|
|
7138
|
+
}
|
|
7139
|
+
/**
|
|
7140
|
+
* 解析官方 file 资源地址(镜像 dsh-util-workspace-path 的 parseFileAddress 语法,不 import 官方包)。
|
|
7141
|
+
* session 域:`dsh-resource://file/session/<sessionId>/<逐段编码的相对路径>`;
|
|
7142
|
+
* absolute 域:`dsh-resource://file/absolute/<去前导 / 的绝对路径>`(POSIX 补回前导 /,
|
|
7143
|
+
* Windows 盘符 `C:/` 原样,UNC 保留空首段 `//server/share`)。
|
|
7144
|
+
* @author ddj 2026年09月09号
|
|
7145
|
+
* @param address 候选地址
|
|
7146
|
+
* @returns 文件路径(session 域附带 sessionId);非 file 地址/畸形/解码失败返回 null
|
|
7147
|
+
*/
|
|
7148
|
+
function parseOfficialFileAddress(address) {
|
|
7149
|
+
if (typeof address !== "string" || !address.startsWith("dsh-resource://file/")) return null;
|
|
7150
|
+
const [scope, ...tail] = address.slice(20).split("/");
|
|
7151
|
+
const kind = decodeSegment(scope);
|
|
7152
|
+
if (kind === "session") {
|
|
7153
|
+
const sessionId = decodeSegment(tail[0]);
|
|
7154
|
+
const segments = tail.slice(1).map(decodeSegment);
|
|
7155
|
+
if (!sessionId || segments.length === 0 || segments.some((s) => s === null)) return null;
|
|
7156
|
+
const path = segments.join("/");
|
|
7157
|
+
return path ? {
|
|
7158
|
+
path,
|
|
7159
|
+
sessionId
|
|
7160
|
+
} : null;
|
|
7161
|
+
}
|
|
7162
|
+
if (kind === "absolute") {
|
|
7163
|
+
const unc = tail[0] === "" && tail.length > 1;
|
|
7164
|
+
const segments = (unc ? tail.slice(1) : tail).map(decodeSegment);
|
|
7165
|
+
if (segments.length === 0 || segments[0] === "" || segments.some((s) => s === null)) return null;
|
|
7166
|
+
const list = segments;
|
|
7167
|
+
if (unc) return { path: "//" + list.join("/") };
|
|
7168
|
+
if (/^[A-Za-z]:$/.test(list[0])) return { path: list.join("/") };
|
|
7169
|
+
return { path: "/" + list.join("/") };
|
|
7170
|
+
}
|
|
7171
|
+
return null;
|
|
7172
|
+
}
|
|
7173
|
+
/**
|
|
7174
|
+
* file 地址的 Tab chip 标题:解码后的 basename(解码失败回退原始串切片)。
|
|
7175
|
+
* @author ddj 2026年09月09号
|
|
7176
|
+
* @param address file 资源地址
|
|
7177
|
+
* @returns 标题文本
|
|
7178
|
+
*/
|
|
7179
|
+
function officialFileTitle(address) {
|
|
7180
|
+
const source = parseOfficialFileAddress(address)?.path ?? (typeof address === "string" ? address : "");
|
|
7181
|
+
const normalized = source.replace(/\\/g, "/");
|
|
7182
|
+
return normalized.slice(normalized.lastIndexOf("/") + 1) || source;
|
|
7183
|
+
}
|
|
7184
|
+
/** component 编码一段(对齐官方 encodeSegment:盘符冒号保持字面)。 */
|
|
7185
|
+
function encodeSegment(segment) {
|
|
7186
|
+
return encodeURIComponent(segment).replace(/%3A/gi, ":");
|
|
7187
|
+
}
|
|
7188
|
+
/**
|
|
7189
|
+
* 构造官方 file 资源地址(镜像 fileAddressFor 的分派:绝对路径→absolute 域,相对→session 域)。
|
|
7190
|
+
* @author ddj 2026年09月09号
|
|
7191
|
+
* @param path 绝对或工作区相对路径(两种分隔符均可)
|
|
7192
|
+
* @param sessionId 会话 id(相对路径必需;绝对路径可省)
|
|
7193
|
+
* @returns `dsh-resource://file/…` 地址
|
|
7194
|
+
* @throws 路径为空,或相对路径缺 sessionId
|
|
7195
|
+
*/
|
|
7196
|
+
function buildFileAddress(path, sessionId) {
|
|
7197
|
+
const posix = (typeof path === "string" ? path : "").replace(/\\/g, "/");
|
|
7198
|
+
if (!posix.trim()) throw new Error("buildFileAddress:路径为空");
|
|
7199
|
+
if (posix.startsWith("/") || posix.startsWith("//") || /^[A-Za-z]:[/\\]/.test(posix)) {
|
|
7200
|
+
const unc = posix.startsWith("//");
|
|
7201
|
+
const body = posix.replace(/^\/+/, "");
|
|
7202
|
+
return "dsh-resource://file/absolute/" + (unc ? "/" : "") + body.split("/").map(encodeSegment).join("/");
|
|
7203
|
+
}
|
|
7204
|
+
if (!sessionId) throw new Error("buildFileAddress:相对路径需要会话 id");
|
|
7205
|
+
const relative = posix.replace(/^(?:\.\/)+/, "").replace(/^\/+/, "");
|
|
7206
|
+
return "dsh-resource://file/session/" + encodeSegment(sessionId) + "/" + relative.split("/").map(encodeSegment).join("/");
|
|
7207
|
+
}
|
|
7208
|
+
/**
|
|
7209
|
+
* 注册官方 Tab 正文(keyed slot,key=定义 id;slots.inject 等待 rightbar seat 声明)。
|
|
7210
|
+
* @author ddj 2026年09月09号
|
|
7211
|
+
* @param slots slots 服务
|
|
7212
|
+
* @param key 正文注册键(= 类型定义 id)
|
|
7213
|
+
* @param renderTab 正文组件(包装后的 OfficialSideTab)
|
|
7214
|
+
* @returns 注销函数;注册抛错返回 null
|
|
7215
|
+
*/
|
|
7216
|
+
function registerBodySlot(slots, key, renderTab) {
|
|
7217
|
+
try {
|
|
7218
|
+
const disposer = slots.inject(OFFICIAL_SLOT_NAME, () => slots.register({
|
|
7219
|
+
name: OFFICIAL_SLOT_NAME,
|
|
7220
|
+
key
|
|
7221
|
+
}, renderTab));
|
|
7222
|
+
return typeof disposer === "function" ? disposer : null;
|
|
7223
|
+
} catch (error) {
|
|
7224
|
+
log.warn("官方侧边栏正文 slot 注册失败(" + String(error) + ")");
|
|
7225
|
+
return null;
|
|
7226
|
+
}
|
|
7227
|
+
}
|
|
7228
|
+
/**
|
|
7229
|
+
* 注册官方右侧 Sidebar「文件编辑」Tab 并接管打开路由(两段式 + 路由)。
|
|
7230
|
+
* 调用方须把返回值挂进 ctx.effect;类型注册抛错时返回 null,由调用方回退旧形态。
|
|
7231
|
+
* @author ddj 2026年09月09号
|
|
7232
|
+
* @param options 装配参数(探测命中的一对服务 + slots + 正文组件 + guide 文案)
|
|
7233
|
+
* @returns 卸载器(反注册类型与正文 + 撤销路由);类型注册失败返回 null
|
|
7234
|
+
*/
|
|
7235
|
+
function installOfficial(options) {
|
|
7236
|
+
const { tabs, service, slots, renderTab, guide } = options;
|
|
7237
|
+
const open = (path, focusDiff) => {
|
|
7238
|
+
try {
|
|
7239
|
+
service.openTab(OFFICIAL_TAB_KIND, { params: {
|
|
7240
|
+
openPath: path || void 0,
|
|
7241
|
+
focusDiff
|
|
7242
|
+
} });
|
|
7243
|
+
} catch (error) {
|
|
7244
|
+
log.warn("官方侧边栏打开失败(" + String(error) + ")");
|
|
7245
|
+
return false;
|
|
7246
|
+
}
|
|
7247
|
+
return true;
|
|
7248
|
+
};
|
|
7249
|
+
let typeDisposer = null;
|
|
7250
|
+
try {
|
|
7251
|
+
typeDisposer = tabs.register({
|
|
7252
|
+
id: OFFICIAL_TAB_ID,
|
|
7253
|
+
kind: OFFICIAL_TAB_KIND,
|
|
7254
|
+
priority: "extension",
|
|
7255
|
+
title: () => OFFICIAL_TAB_TITLE,
|
|
7256
|
+
guide: [{
|
|
7257
|
+
order: 20,
|
|
7258
|
+
title: guide.title,
|
|
7259
|
+
description: guide.description
|
|
7260
|
+
}]
|
|
7261
|
+
});
|
|
7262
|
+
} catch (error) {
|
|
7263
|
+
log.warn("官方侧边栏 Tab 类型注册失败(" + String(error) + "),回退旧形态");
|
|
7264
|
+
return null;
|
|
7265
|
+
}
|
|
7266
|
+
setEnsureSideEditor(open);
|
|
7267
|
+
const bodyDisposer = registerBodySlot(slots, OFFICIAL_TAB_ID, renderTab);
|
|
7268
|
+
return () => {
|
|
7269
|
+
if (typeof typeDisposer === "function") typeDisposer();
|
|
7270
|
+
if (typeof bodyDisposer === "function") bodyDisposer();
|
|
7271
|
+
setEnsureSideEditor(null);
|
|
7272
|
+
};
|
|
7273
|
+
}
|
|
7274
|
+
/**
|
|
7275
|
+
* 注册官方 file 资源地址认领(extension 档接管 `dsh-resource://file/**`,
|
|
7276
|
+
* 聊天文件链接/文件树打开改落本插件编辑器;注销后官方 textpreview 自动恢复)。
|
|
7277
|
+
* 认领与否由「文件链接使用工具」设置驱动(见 fileOpeners.shouldClaimFiles)。
|
|
7278
|
+
* @author ddj 2026年09月09号
|
|
7279
|
+
* @param options 装配参数(探测命中的注册表 + slots + 正文组件)
|
|
7280
|
+
* @returns 卸载器(反注册类型与正文);类型注册抛错返回 null(回落官方查看器)
|
|
7281
|
+
*/
|
|
7282
|
+
function registerOfficialFileClaim(options) {
|
|
7283
|
+
const { tabs, slots, renderTab } = options;
|
|
7284
|
+
let typeDisposer = null;
|
|
7285
|
+
try {
|
|
7286
|
+
typeDisposer = tabs.register({
|
|
7287
|
+
id: OFFICIAL_FILE_TAB_ID,
|
|
7288
|
+
kind: OFFICIAL_FILE_KIND,
|
|
7289
|
+
patterns: [OFFICIAL_FILE_PATTERN],
|
|
7290
|
+
priority: "extension",
|
|
7291
|
+
canOpen: (address) => parseOfficialFileAddress(address) !== null,
|
|
7292
|
+
title: (address) => officialFileTitle(address)
|
|
7293
|
+
});
|
|
7294
|
+
} catch (error) {
|
|
7295
|
+
log.warn("官方 file 地址认领注册失败(" + String(error) + "),链接走官方查看器");
|
|
7296
|
+
return null;
|
|
7297
|
+
}
|
|
7298
|
+
const bodyDisposer = registerBodySlot(slots, OFFICIAL_FILE_TAB_ID, renderTab);
|
|
7299
|
+
return () => {
|
|
7300
|
+
if (typeof typeDisposer === "function") typeDisposer();
|
|
7301
|
+
if (typeof bodyDisposer === "function") bodyDisposer();
|
|
7302
|
+
};
|
|
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
|
+
}
|
|
7357
|
+
//#endregion
|
|
6886
7358
|
//#region src/client/fileOpeners.ts
|
|
6887
7359
|
/**
|
|
6888
7360
|
* dsh-vscode-mode client — 文件打开器注册表与自动选择。
|
|
6889
|
-
* @author ddj 2026年08月24号
|
|
7361
|
+
* @author ddj 2026年08月24号 / 2026年09月09号
|
|
6890
7362
|
*/
|
|
6891
7363
|
const AUTO_OPEN_TOOL = "auto";
|
|
6892
7364
|
const VSCODE_OPEN_TOOL = "dsh-vscode-mode";
|
|
6893
7365
|
const SIDEBAR_OPEN_TOOL = SIDEBAR_PLUGIN;
|
|
7366
|
+
/** 官方右侧侧边栏打开器 id(「文件链接使用工具」下拉的官方选项;DSH 0.1.5+)。 */
|
|
7367
|
+
const OFFICIAL_OPEN_TOOL = "dsh-official-sidebar";
|
|
6894
7368
|
/**
|
|
6895
7369
|
* 写入外部打开器并触发注册表通知。
|
|
6896
7370
|
* @author ddj 2026年08月24号
|
|
@@ -7016,6 +7490,36 @@ window.__ModuleLoader__.load({
|
|
|
7016
7490
|
const normalized = path.replace(/\\/g, "/");
|
|
7017
7491
|
return normalized.slice(normalized.lastIndexOf("/") + 1) || path;
|
|
7018
7492
|
}
|
|
7493
|
+
/**
|
|
7494
|
+
* 文件链接是否由本插件认领官方 file 地址:自动/VSCodeMode 档接管(链接进本插件编辑器),
|
|
7495
|
+
* 官方侧边栏/system/better-sidebar 档不认领(官方查看器或旧版路由生效)。
|
|
7496
|
+
* @author ddj 2026年09月09号
|
|
7497
|
+
* @param selected 「文件链接使用工具」当前值
|
|
7498
|
+
* @returns 是否认领
|
|
7499
|
+
*/
|
|
7500
|
+
function shouldClaimFiles(selected) {
|
|
7501
|
+
return selected === "auto" || selected === "dsh-vscode-mode";
|
|
7502
|
+
}
|
|
7503
|
+
/**
|
|
7504
|
+
* 创建「官方侧边栏」文件打开器(DSH 0.1.5+,经 ctx.sidebarRight.openResource 打开官方查看器)。
|
|
7505
|
+
* 仅官方服务探测命中时注册;下拉选项随注册动态出现(McpSettings 动态渲染)。
|
|
7506
|
+
* @author ddj 2026年09月09号
|
|
7507
|
+
* @param service 官方导航控制器
|
|
7508
|
+
* @returns 文件打开器
|
|
7509
|
+
*/
|
|
7510
|
+
function officialSidebarOpener(service) {
|
|
7511
|
+
return {
|
|
7512
|
+
id: OFFICIAL_OPEN_TOOL,
|
|
7513
|
+
label: "官方侧边栏",
|
|
7514
|
+
description: "在 DSH 官方右侧侧边栏查看器中打开(0.1.5+)",
|
|
7515
|
+
priority: 90,
|
|
7516
|
+
isAvailable: () => typeof service.openResource === "function",
|
|
7517
|
+
open: (path, context) => {
|
|
7518
|
+
if (typeof service.openResource !== "function") throw new Error("官方侧边栏 openResource 能力不可用");
|
|
7519
|
+
service.openResource(buildFileAddress(path, context.sessionId));
|
|
7520
|
+
}
|
|
7521
|
+
};
|
|
7522
|
+
}
|
|
7019
7523
|
//#endregion
|
|
7020
7524
|
//#region src/client/settingsContext.ts
|
|
7021
7525
|
/**
|
|
@@ -9578,6 +10082,83 @@ window.__ModuleLoader__.load({
|
|
|
9578
10082
|
});
|
|
9579
10083
|
}
|
|
9580
10084
|
//#endregion
|
|
10085
|
+
//#region src/client/ui/OfficialSideTab.ts
|
|
10086
|
+
/**
|
|
10087
|
+
* dsh-vscode-mode client — OfficialSideTab:DSH 官方右侧 Sidebar「文件编辑」Tab 正文包装。
|
|
10088
|
+
* 双用途:页类型 Tab(edrvEditor)经 navigation.params(openPath/focusDiff)打开;
|
|
10089
|
+
* file 资源类型 Tab(edrvEditorFile,认领 dsh-resource://file/**)经 navigation.address
|
|
10090
|
+
* 解码出真实路径并透传 params.line 行号定位。派发既有 edrv:open-editor 事件复用
|
|
10091
|
+
* EditorView 打开流;按会话 key 保证切会话干净重建。
|
|
10092
|
+
* Tab 正文切换后重挂载时按最新导航恢复打开(对齐 better-sidebar 形态的种子行为)。
|
|
10093
|
+
* 作者 ddj 2026年09月09号
|
|
10094
|
+
*/
|
|
10095
|
+
/**
|
|
10096
|
+
* 官方侧边栏 Tab 正文组件(keyed slot sidebar.right.pane.tab 装配)。
|
|
10097
|
+
* @param props slot 框架注入(useTabInfo/sessionId)+ 装配依赖(schedule 等)
|
|
10098
|
+
* @returns EditorView 侧栏形态
|
|
10099
|
+
*/
|
|
10100
|
+
function OfficialSideTab(props) {
|
|
10101
|
+
const sessionId = props?.sessionId;
|
|
10102
|
+
const useTabInfo = props?.useTabInfo;
|
|
10103
|
+
let navigation;
|
|
10104
|
+
if (typeof useTabInfo === "function") try {
|
|
10105
|
+
navigation = useTabInfo()?.tab?.navigation;
|
|
10106
|
+
} catch {
|
|
10107
|
+
navigation = void 0;
|
|
10108
|
+
}
|
|
10109
|
+
const revision = typeof navigation?.revision === "number" ? navigation.revision : -1;
|
|
10110
|
+
const revisionRef = react.default.useRef(-1);
|
|
10111
|
+
const sessionIdRef = react.default.useRef(sessionId);
|
|
10112
|
+
sessionIdRef.current = sessionId;
|
|
10113
|
+
react.default.useEffect(() => {
|
|
10114
|
+
setSideEditorMounted(true);
|
|
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
|
+
};
|
|
10129
|
+
}, []);
|
|
10130
|
+
react.default.useEffect(() => {
|
|
10131
|
+
if (revision === revisionRef.current) return;
|
|
10132
|
+
revisionRef.current = revision;
|
|
10133
|
+
if (revision <= 0) return;
|
|
10134
|
+
const fileHit = parseOfficialFileAddress(navigation?.address);
|
|
10135
|
+
if (fileHit) {
|
|
10136
|
+
const line = navigation?.params?.line;
|
|
10137
|
+
window.dispatchEvent(new CustomEvent("edrv:open-editor", { detail: {
|
|
10138
|
+
path: fileHit.path,
|
|
10139
|
+
line: typeof line === "number" ? line : void 0
|
|
10140
|
+
} }));
|
|
10141
|
+
return;
|
|
10142
|
+
}
|
|
10143
|
+
const request = resolveNavOpen(navigation?.params);
|
|
10144
|
+
window.dispatchEvent(new CustomEvent("edrv:open-editor", { detail: {
|
|
10145
|
+
path: request.path,
|
|
10146
|
+
focusDiff: request.focusDiff
|
|
10147
|
+
} }));
|
|
10148
|
+
});
|
|
10149
|
+
return react.default.createElement(EditorView, {
|
|
10150
|
+
key: sessionId,
|
|
10151
|
+
sessionId,
|
|
10152
|
+
layout: "side",
|
|
10153
|
+
schedule: props?.schedule,
|
|
10154
|
+
addToConversation: props?.addToConversation,
|
|
10155
|
+
sidebarPanels: props?.sidebarPanels,
|
|
10156
|
+
outlineSources: props?.outlineSources,
|
|
10157
|
+
fileMenuItems: props?.fileMenuItems,
|
|
10158
|
+
sessions: props?.sessions
|
|
10159
|
+
});
|
|
10160
|
+
}
|
|
10161
|
+
//#endregion
|
|
9581
10162
|
//#region src/client/sidebar/registry.ts
|
|
9582
10163
|
/** 校验面板定义并写入注册表(缺 id/render 抛 TypeError)。 */
|
|
9583
10164
|
function panelRegister(entries, notify, panel) {
|
|
@@ -9771,7 +10352,7 @@ window.__ModuleLoader__.load({
|
|
|
9771
10352
|
* 键前缀统一走 paths.ts PathConst(CACHE_KEY.expanded)。
|
|
9772
10353
|
* 作者 ddj 2026-08-28
|
|
9773
10354
|
*/
|
|
9774
|
-
/** localStorage
|
|
10355
|
+
/** localStorage 键前缀(按工作区作用域隔离,无 cwd 会话回退会话键)。 */
|
|
9775
10356
|
const KEY_PREFIX$1 = CACHE_KEY.expanded;
|
|
9776
10357
|
/**
|
|
9777
10358
|
* 归一化展开路径:去空白、反斜杠→斜杠、去首尾斜杠,`..` 段拒绝。
|
|
@@ -9843,27 +10424,27 @@ window.__ModuleLoader__.load({
|
|
|
9843
10424
|
}
|
|
9844
10425
|
}
|
|
9845
10426
|
/**
|
|
9846
|
-
*
|
|
10427
|
+
* 读取某作用域的展开状态缓存(localStorage 不可用 → null)。
|
|
9847
10428
|
* @author ddj 2026年08月28号
|
|
9848
|
-
* @param
|
|
10429
|
+
* @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
|
|
9849
10430
|
* @returns 缓存数据或 null
|
|
9850
10431
|
*/
|
|
9851
|
-
function explorerLoad(
|
|
10432
|
+
function explorerLoad(scope) {
|
|
9852
10433
|
try {
|
|
9853
|
-
return parseExplorer(window.localStorage.getItem(KEY_PREFIX$1 + String(
|
|
10434
|
+
return parseExplorer(window.localStorage.getItem(KEY_PREFIX$1 + String(scope)));
|
|
9854
10435
|
} catch (error) {
|
|
9855
10436
|
return null;
|
|
9856
10437
|
}
|
|
9857
10438
|
}
|
|
9858
10439
|
/**
|
|
9859
|
-
*
|
|
10440
|
+
* 写入某作用域的展开状态缓存(损坏/配额满 → 忽略)。
|
|
9860
10441
|
* @author ddj 2026年08月28号
|
|
9861
|
-
* @param
|
|
10442
|
+
* @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
|
|
9862
10443
|
* @param data 缓存数据
|
|
9863
10444
|
*/
|
|
9864
|
-
function explorerSave(
|
|
10445
|
+
function explorerSave(scope, data) {
|
|
9865
10446
|
try {
|
|
9866
|
-
window.localStorage.setItem(KEY_PREFIX$1 + String(
|
|
10447
|
+
window.localStorage.setItem(KEY_PREFIX$1 + String(scope), serializeExplorer(data));
|
|
9867
10448
|
} catch (error) {}
|
|
9868
10449
|
}
|
|
9869
10450
|
//#endregion
|
|
@@ -9928,9 +10509,9 @@ window.__ModuleLoader__.load({
|
|
|
9928
10509
|
return null;
|
|
9929
10510
|
}
|
|
9930
10511
|
}
|
|
9931
|
-
/**
|
|
10512
|
+
/** scope → 缓存镜像(同步读,避免每次渲染 parse)。 */
|
|
9932
10513
|
const mirrors = /* @__PURE__ */ new Map();
|
|
9933
|
-
/**
|
|
10514
|
+
/** scope → 落盘防抖计时器。 */
|
|
9934
10515
|
const saveTimers = /* @__PURE__ */ new Map();
|
|
9935
10516
|
/** 单调时钟:连续 put 的 ts 严格递增(同毫秒不并列,逐出顺序可预期)。 */
|
|
9936
10517
|
let clock = 0;
|
|
@@ -9940,82 +10521,82 @@ window.__ModuleLoader__.load({
|
|
|
9940
10521
|
return clock;
|
|
9941
10522
|
}
|
|
9942
10523
|
/** localStorage 读取(不可用 → null)。 */
|
|
9943
|
-
function readRaw(
|
|
10524
|
+
function readRaw(scope) {
|
|
9944
10525
|
try {
|
|
9945
|
-
return window.localStorage.getItem(KEY_PREFIX +
|
|
10526
|
+
return window.localStorage.getItem(KEY_PREFIX + scope);
|
|
9946
10527
|
} catch (error) {
|
|
9947
10528
|
return null;
|
|
9948
10529
|
}
|
|
9949
10530
|
}
|
|
9950
10531
|
/** localStorage 写入(配额/隐私模式失败静默)。 */
|
|
9951
|
-
function writeRaw(
|
|
10532
|
+
function writeRaw(scope, text) {
|
|
9952
10533
|
try {
|
|
9953
|
-
window.localStorage.setItem(KEY_PREFIX +
|
|
10534
|
+
window.localStorage.setItem(KEY_PREFIX + scope, text);
|
|
9954
10535
|
} catch (error) {}
|
|
9955
10536
|
}
|
|
9956
|
-
/**
|
|
9957
|
-
function mirrorOf(
|
|
9958
|
-
const existing = mirrors.get(
|
|
10537
|
+
/** 取(或建)作用域缓存镜像。 */
|
|
10538
|
+
function mirrorOf(scope) {
|
|
10539
|
+
const existing = mirrors.get(scope);
|
|
9959
10540
|
if (existing) return existing;
|
|
9960
|
-
const data = entriesParse(readRaw(
|
|
10541
|
+
const data = entriesParse(readRaw(scope)) ?? {
|
|
9961
10542
|
v: 2,
|
|
9962
10543
|
root: null,
|
|
9963
10544
|
dirs: {}
|
|
9964
10545
|
};
|
|
9965
|
-
mirrors.set(
|
|
10546
|
+
mirrors.set(scope, data);
|
|
9966
10547
|
return data;
|
|
9967
10548
|
}
|
|
9968
10549
|
/** 调度防抖落盘(重置已有计时器)。 */
|
|
9969
|
-
function scheduleSave(
|
|
9970
|
-
const prev = saveTimers.get(
|
|
10550
|
+
function scheduleSave(scope) {
|
|
10551
|
+
const prev = saveTimers.get(scope);
|
|
9971
10552
|
if (prev) clearTimeout(prev);
|
|
9972
|
-
saveTimers.set(
|
|
9973
|
-
saveTimers.delete(
|
|
9974
|
-
const data = mirrors.get(
|
|
9975
|
-
if (data) writeRaw(
|
|
10553
|
+
saveTimers.set(scope, setTimeout(() => {
|
|
10554
|
+
saveTimers.delete(scope);
|
|
10555
|
+
const data = mirrors.get(scope);
|
|
10556
|
+
if (data) writeRaw(scope, entriesSerialize(entriesTrim(data)));
|
|
9976
10557
|
}, 300));
|
|
9977
10558
|
}
|
|
9978
10559
|
/**
|
|
9979
10560
|
* 读某目录缓存条目(无 → null;展示不受新鲜度限制)。
|
|
9980
10561
|
* @author ddj 2026年08月31号
|
|
9981
|
-
* @param
|
|
10562
|
+
* @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
|
|
9982
10563
|
* @param rel 目录相对路径('' = 根)
|
|
9983
10564
|
* @returns 缓存条目或 null
|
|
9984
10565
|
*/
|
|
9985
|
-
function entriesCacheGet(
|
|
9986
|
-
if (!
|
|
9987
|
-
const dir = mirrorOf(
|
|
10566
|
+
function entriesCacheGet(scope, rel) {
|
|
10567
|
+
if (!scope) return null;
|
|
10568
|
+
const dir = mirrorOf(scope).dirs[rel];
|
|
9988
10569
|
if (!dir || !dir.entries.length) return null;
|
|
9989
10570
|
return dir.entries;
|
|
9990
10571
|
}
|
|
9991
10572
|
/**
|
|
9992
10573
|
* 某目录缓存是否新鲜(预取去重用)。
|
|
9993
10574
|
* @author ddj 2026年08月31号
|
|
9994
|
-
* @param
|
|
10575
|
+
* @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
|
|
9995
10576
|
* @param rel 目录相对路径
|
|
9996
10577
|
* @returns 新鲜返回 true
|
|
9997
10578
|
*/
|
|
9998
|
-
function entriesCacheIsFresh(
|
|
9999
|
-
if (!
|
|
10000
|
-
const dir = mirrorOf(
|
|
10579
|
+
function entriesCacheIsFresh(scope, rel) {
|
|
10580
|
+
if (!scope) return false;
|
|
10581
|
+
const dir = mirrorOf(scope).dirs[rel];
|
|
10001
10582
|
return Boolean(dir && Date.now() - dir.ts < 3e4);
|
|
10002
10583
|
}
|
|
10003
10584
|
/**
|
|
10004
10585
|
* 写入某目录缓存条目(内存镜像 + 防抖落盘;单目录条目截断)。
|
|
10005
10586
|
* @author ddj 2026年08月31号
|
|
10006
|
-
* @param
|
|
10587
|
+
* @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
|
|
10007
10588
|
* @param rel 目录相对路径
|
|
10008
10589
|
* @param entries 目录条目
|
|
10009
10590
|
*/
|
|
10010
|
-
function entriesCachePut(
|
|
10011
|
-
if (!
|
|
10012
|
-
const data = mirrorOf(
|
|
10591
|
+
function entriesCachePut(scope, rel, entries) {
|
|
10592
|
+
if (!scope) return;
|
|
10593
|
+
const data = mirrorOf(scope);
|
|
10013
10594
|
data.dirs[rel] = {
|
|
10014
10595
|
ts: nextTs(),
|
|
10015
10596
|
entries: entries.slice(0, 500)
|
|
10016
10597
|
};
|
|
10017
10598
|
data.dirs = entriesTrim(data).dirs;
|
|
10018
|
-
scheduleSave(
|
|
10599
|
+
scheduleSave(scope);
|
|
10019
10600
|
}
|
|
10020
10601
|
//#endregion
|
|
10021
10602
|
//#region src/client/sidebar/panels/FileExplorer.ts
|
|
@@ -10047,6 +10628,7 @@ window.__ModuleLoader__.load({
|
|
|
10047
10628
|
function FileExplorer(props) {
|
|
10048
10629
|
const ctx = props?.ctx;
|
|
10049
10630
|
const sessionId = ctx?.sessionId;
|
|
10631
|
+
const scope = ctx?.scope ?? workspaceScopeOf(null, sessionId);
|
|
10050
10632
|
const openFile = ctx?.openFile;
|
|
10051
10633
|
const activePath = ctx?.activePath ?? null;
|
|
10052
10634
|
const pendingByPath = ctx?.pendingByPath ?? {};
|
|
@@ -10067,14 +10649,14 @@ window.__ModuleLoader__.load({
|
|
|
10067
10649
|
const loadDirRef = react.default.useRef(null);
|
|
10068
10650
|
const refreshRef = react.default.useRef(null);
|
|
10069
10651
|
/** 渲染取数:内存态 → 本地条目缓存 → null(显示加载态)。 */
|
|
10070
|
-
const entriesOf = (rel) => dirsRef.current[rel] ?? entriesCacheGet(
|
|
10652
|
+
const entriesOf = (rel) => dirsRef.current[rel] ?? entriesCacheGet(scope, rel) ?? null;
|
|
10071
10653
|
/** 预取子目录:≤4 个、排除重型目录、缓存新鲜跳过、已加载跳过、不级联。 */
|
|
10072
10654
|
const prefetchDirs = (rel, entries) => {
|
|
10073
10655
|
const want = [];
|
|
10074
10656
|
for (const e of entries) {
|
|
10075
10657
|
if (e.type !== "directory") continue;
|
|
10076
10658
|
if (PREFETCH_EXCLUDED.has(e.name)) continue;
|
|
10077
|
-
if (entriesCacheIsFresh(
|
|
10659
|
+
if (entriesCacheIsFresh(scope, e.path)) continue;
|
|
10078
10660
|
if (dirsRef.current[e.path] !== void 0) continue;
|
|
10079
10661
|
want.push(e.path);
|
|
10080
10662
|
if (want.length >= PREFETCH_MAX) break;
|
|
@@ -10107,7 +10689,7 @@ window.__ModuleLoader__.load({
|
|
|
10107
10689
|
if (res && res.ok && Array.isArray(res.entries)) {
|
|
10108
10690
|
setDirs((prev) => Object.assign({}, prev, { [rel]: res.entries }));
|
|
10109
10691
|
if (res.root) setRoot((prev) => prev || res.root);
|
|
10110
|
-
entriesCachePut(
|
|
10692
|
+
entriesCachePut(scope, rel, res.entries);
|
|
10111
10693
|
if (doPrefetch) prefetchDirs(rel, res.entries);
|
|
10112
10694
|
} else if (!entriesOf(rel)) setErrors((prev) => Object.assign({}, prev, { [rel]: res?.error ? String(res.error) : "读取目录失败" }));
|
|
10113
10695
|
}).catch((e) => {
|
|
@@ -10160,7 +10742,7 @@ window.__ModuleLoader__.load({
|
|
|
10160
10742
|
setErrors({});
|
|
10161
10743
|
setMenu(null);
|
|
10162
10744
|
setRoot(null);
|
|
10163
|
-
const cached =
|
|
10745
|
+
const cached = scope ? explorerLoad(scope) : null;
|
|
10164
10746
|
const restored = cached?.expanded ?? [];
|
|
10165
10747
|
if (cached?.root) setRoot(cached.root);
|
|
10166
10748
|
if (restored.length) {
|
|
@@ -10170,17 +10752,17 @@ window.__ModuleLoader__.load({
|
|
|
10170
10752
|
}
|
|
10171
10753
|
loadDir("", { prefetch: true });
|
|
10172
10754
|
for (const rel of restored) if (rel) loadDir(rel, { prefetch: true });
|
|
10173
|
-
}, [
|
|
10755
|
+
}, [scope]);
|
|
10174
10756
|
react.default.useEffect(() => {
|
|
10175
10757
|
if (saveTimerRef.current) {
|
|
10176
10758
|
clearTimeout(saveTimerRef.current);
|
|
10177
10759
|
saveTimerRef.current = null;
|
|
10178
10760
|
}
|
|
10179
|
-
if (!
|
|
10761
|
+
if (!scope) return;
|
|
10180
10762
|
saveTimerRef.current = setTimeout(() => {
|
|
10181
10763
|
saveTimerRef.current = null;
|
|
10182
10764
|
const expandedList = Object.keys(expanded).filter((k) => expanded[k] === true);
|
|
10183
|
-
explorerSave(
|
|
10765
|
+
explorerSave(scope, {
|
|
10184
10766
|
root,
|
|
10185
10767
|
expanded: expandedList
|
|
10186
10768
|
});
|
|
@@ -10194,7 +10776,7 @@ window.__ModuleLoader__.load({
|
|
|
10194
10776
|
}, [
|
|
10195
10777
|
expanded,
|
|
10196
10778
|
root,
|
|
10197
|
-
|
|
10779
|
+
scope
|
|
10198
10780
|
]);
|
|
10199
10781
|
react.default.useEffect(() => {
|
|
10200
10782
|
const onRefresh = () => refreshRef.current?.();
|
|
@@ -10202,14 +10784,14 @@ window.__ModuleLoader__.load({
|
|
|
10202
10784
|
return () => window.removeEventListener("edrv:refresh", onRefresh);
|
|
10203
10785
|
}, []);
|
|
10204
10786
|
react.default.useEffect(() => {
|
|
10205
|
-
if (!
|
|
10787
|
+
if (!scope) return;
|
|
10206
10788
|
const timer = setInterval(() => {
|
|
10207
10789
|
const open = Object.keys(expandedRef.current).filter((k) => expandedRef.current[k] === true);
|
|
10208
10790
|
if (!open.length) return;
|
|
10209
10791
|
for (const rel of open) loadDirRef.current?.(rel, {});
|
|
10210
10792
|
}, FOLLOW_INTERVAL_MS);
|
|
10211
10793
|
return () => clearInterval(timer);
|
|
10212
|
-
}, [
|
|
10794
|
+
}, [scope]);
|
|
10213
10795
|
const rootName = root ? String(root).split(/[\\/]/).pop() || root : "";
|
|
10214
10796
|
const rowEl = (e, depth, isDir, isOpen) => {
|
|
10215
10797
|
const pending = isDir ? 0 : pendingByPath[e.path] ?? 0;
|
|
@@ -10348,6 +10930,7 @@ window.__ModuleLoader__.load({
|
|
|
10348
10930
|
function SearchPanel(props) {
|
|
10349
10931
|
const ctx = props?.ctx;
|
|
10350
10932
|
const sessionId = ctx?.sessionId;
|
|
10933
|
+
const scope = ctx?.scope ?? workspaceScopeOf(null, sessionId);
|
|
10351
10934
|
const openFileAt = ctx?.openFileAt ?? (() => {});
|
|
10352
10935
|
const activePath = ctx?.activePath ?? null;
|
|
10353
10936
|
const [query, setQuery] = react.default.useState("");
|
|
@@ -10387,9 +10970,9 @@ window.__ModuleLoader__.load({
|
|
|
10387
10970
|
exclude: excludeOn ? splitGlobs(excludeText) : []
|
|
10388
10971
|
};
|
|
10389
10972
|
react.default.useEffect(() => {
|
|
10390
|
-
if (!
|
|
10973
|
+
if (!scope) return;
|
|
10391
10974
|
try {
|
|
10392
|
-
const saved = JSON.parse(localStorage.getItem(CACHE_KEY.search + String(
|
|
10975
|
+
const saved = JSON.parse(localStorage.getItem(CACHE_KEY.search + String(scope)) || "null");
|
|
10393
10976
|
if (saved && typeof saved === "object") {
|
|
10394
10977
|
if (typeof saved.query === "string") setQuery(saved.query);
|
|
10395
10978
|
if (typeof saved.matchCase === "boolean") setMatchCase(saved.matchCase);
|
|
@@ -10402,11 +10985,11 @@ window.__ModuleLoader__.load({
|
|
|
10402
10985
|
if (typeof saved.sectionOpen === "boolean") setSectionOpen(saved.sectionOpen);
|
|
10403
10986
|
}
|
|
10404
10987
|
} catch (e) {}
|
|
10405
|
-
}, [
|
|
10988
|
+
}, [scope]);
|
|
10406
10989
|
react.default.useEffect(() => {
|
|
10407
|
-
if (!
|
|
10990
|
+
if (!scope) return;
|
|
10408
10991
|
try {
|
|
10409
|
-
localStorage.setItem(CACHE_KEY.search + String(
|
|
10992
|
+
localStorage.setItem(CACHE_KEY.search + String(scope), JSON.stringify({
|
|
10410
10993
|
query,
|
|
10411
10994
|
matchCase,
|
|
10412
10995
|
wholeWord,
|
|
@@ -10419,7 +11002,7 @@ window.__ModuleLoader__.load({
|
|
|
10419
11002
|
}));
|
|
10420
11003
|
} catch (e) {}
|
|
10421
11004
|
}, [
|
|
10422
|
-
|
|
11005
|
+
scope,
|
|
10423
11006
|
query,
|
|
10424
11007
|
matchCase,
|
|
10425
11008
|
wholeWord,
|
|
@@ -12232,8 +12815,8 @@ window.__ModuleLoader__.load({
|
|
|
12232
12815
|
//#region src/client/index.ts
|
|
12233
12816
|
/**
|
|
12234
12817
|
* dsh-vscode-mode client — 浏览器半入口:slot 注册 + 装配。
|
|
12235
|
-
*
|
|
12236
|
-
*
|
|
12818
|
+
* 挂点:官方右侧 Sidebar「文件编辑」Tab(DSH 0.1.5+,对话+编辑同屏)> betterSidebar Tab
|
|
12819
|
+
* (归档,仅旧版 DSH 回退)> conversation.view 中央页签 + conversation.input.dock 差异条 + header 差异角标。
|
|
12237
12820
|
* 与 Host 通信:同源 fetch('/edrv/rpc')(shared/rpc 契约)。
|
|
12238
12821
|
*
|
|
12239
12822
|
* ⚠️ 跨版本 slot 装配(2026-08-21):新版 DSH 的 slots 系统要求 slot 必须由父 entry
|
|
@@ -12290,6 +12873,23 @@ window.__ModuleLoader__.load({
|
|
|
12290
12873
|
setSession(list.getSnapshot()?.current);
|
|
12291
12874
|
}), "vscode-mode: lsp session sync");
|
|
12292
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
|
+
}
|
|
12293
12893
|
const originalOpenPath = workspaces?.openPath;
|
|
12294
12894
|
const binder = pickSettingsBinder(ctx);
|
|
12295
12895
|
const settings = binder.scope;
|
|
@@ -12309,6 +12909,10 @@ window.__ModuleLoader__.load({
|
|
|
12309
12909
|
};
|
|
12310
12910
|
/** 可选探测 betterSidebar 服务(不进 inject:缺失会让插件停靠等待,杀死回退路径)。 */
|
|
12311
12911
|
let sideService = detectSidebarService(ctx);
|
|
12912
|
+
/** 可选探测官方右侧 Sidebar(DSH 0.1.5-alpha.1+;探测命中 ≡ 版本判定,不进 inject 同上)。 */
|
|
12913
|
+
let officialService = detectOfficial(ctx);
|
|
12914
|
+
/** 官方 file 地址认领的当前注销器(null=未认领;由 syncFileClaim 依设置值切换)。 */
|
|
12915
|
+
let claimDisposer = null;
|
|
12312
12916
|
/** 「添加到对话」动作集:编辑区/Tab 右键菜单注入文件引用与代码块(conversation 服务缺失时各动作安全降级)。 */
|
|
12313
12917
|
const addToConversation = createAddToConversation(ctx);
|
|
12314
12918
|
/** 客户端侧兼容摘要(与 host 报告合并展示;惰性求值保证 HMR 后仍新鲜)。 */
|
|
@@ -12334,9 +12938,19 @@ window.__ModuleLoader__.load({
|
|
|
12334
12938
|
note: registry.get("dsh-better-sidebar") !== void 0 ? "已注册(优先级 80)" : "未检测到侧边栏打开能力"
|
|
12335
12939
|
},
|
|
12336
12940
|
{
|
|
12337
|
-
name: "
|
|
12941
|
+
name: "侧边栏编辑区(官方 Sidebar)",
|
|
12942
|
+
active: officialService !== void 0,
|
|
12943
|
+
note: officialService !== void 0 ? "编辑区=官方右侧 Sidebar Tab(对话+编辑同屏,DSH 0.1.5+)" : "官方侧边栏服务未探测到(DSH < 0.1.5-alpha.1 时属预期)"
|
|
12944
|
+
},
|
|
12945
|
+
{
|
|
12946
|
+
name: "侧边栏编辑区(dsh-better-sidebar,归档)",
|
|
12338
12947
|
active: sideService !== void 0,
|
|
12339
|
-
note: sideService !== void 0 ? "编辑区=侧边栏 Tab
|
|
12948
|
+
note: sideService !== void 0 ? "编辑区=侧边栏 Tab(旧版 DSH 回退形态)" : "未检测到;DSH ≥ 0.1.5 优先官方侧边栏,旧版可安装(" + SIDEBAR_INSTALL_CMD + ")"
|
|
12949
|
+
},
|
|
12950
|
+
{
|
|
12951
|
+
name: "文件链接官方认领(dsh-resource://file)",
|
|
12952
|
+
active: claimDisposer !== null,
|
|
12953
|
+
note: claimDisposer !== null ? "聊天文件链接由本插件编辑器接管(官方侧边栏内打开)" : "链接走官方查看器或旧版路由(未认领)"
|
|
12340
12954
|
}
|
|
12341
12955
|
];
|
|
12342
12956
|
ctx.provide("fileOpeners", registry);
|
|
@@ -12364,12 +12978,39 @@ window.__ModuleLoader__.load({
|
|
|
12364
12978
|
const sidebar = scanSidebar(ctx);
|
|
12365
12979
|
return sidebar ? registry.register(sidebar) : void 0;
|
|
12366
12980
|
}, "vscode-mode: sidebar file opener");
|
|
12981
|
+
const officialRenderTab = (props) => react.default.createElement(OfficialSideTab, Object.assign({}, props, {
|
|
12982
|
+
schedule,
|
|
12983
|
+
addToConversation,
|
|
12984
|
+
sidebarPanels,
|
|
12985
|
+
outlineSources,
|
|
12986
|
+
fileMenuItems,
|
|
12987
|
+
sessions
|
|
12988
|
+
}));
|
|
12989
|
+
/** 官方 file 地址认领同步:自动/VSCodeMode 档认领(链接进本插件编辑器),其余交官方查看器。 */
|
|
12990
|
+
const syncFileClaim = () => {
|
|
12991
|
+
const official = officialService;
|
|
12992
|
+
const want = official !== void 0 && shouldClaimFiles(selected);
|
|
12993
|
+
if (want && claimDisposer === null && official) {
|
|
12994
|
+
const disposer = registerOfficialFileClaim({
|
|
12995
|
+
tabs: official.tabs,
|
|
12996
|
+
slots: ctx.slots,
|
|
12997
|
+
renderTab: officialRenderTab
|
|
12998
|
+
});
|
|
12999
|
+
if (disposer !== null) claimDisposer = ctx.effect(() => disposer, "vscode-mode: official file claim");
|
|
13000
|
+
return;
|
|
13001
|
+
}
|
|
13002
|
+
if (!want && claimDisposer !== null) {
|
|
13003
|
+
claimDisposer();
|
|
13004
|
+
claimDisposer = null;
|
|
13005
|
+
}
|
|
13006
|
+
};
|
|
12367
13007
|
ctx.effect(() => {
|
|
12368
13008
|
if (!settings) return void 0;
|
|
12369
13009
|
const sync = () => {
|
|
12370
13010
|
const snapshot = settings.getSnapshot();
|
|
12371
13011
|
if (snapshot.status === "loading") return;
|
|
12372
13012
|
selected = autoValue(snapshot.value?.fileOpenTool);
|
|
13013
|
+
syncFileClaim();
|
|
12373
13014
|
window.dispatchEvent(new CustomEvent("edrv:file-open-tool-change", { detail: { value: selected } }));
|
|
12374
13015
|
};
|
|
12375
13016
|
sync();
|
|
@@ -12442,11 +13083,50 @@ window.__ModuleLoader__.load({
|
|
|
12442
13083
|
sessions
|
|
12443
13084
|
})));
|
|
12444
13085
|
};
|
|
13086
|
+
let officialDisposer = null;
|
|
12445
13087
|
let sideDisposer = null;
|
|
12446
13088
|
let legacyDisposer = null;
|
|
13089
|
+
const installOfficialForm = () => {
|
|
13090
|
+
const official = officialService;
|
|
13091
|
+
if (officialDisposer !== null || !official) return;
|
|
13092
|
+
if (sideDisposer !== null) {
|
|
13093
|
+
sideDisposer();
|
|
13094
|
+
sideDisposer = null;
|
|
13095
|
+
}
|
|
13096
|
+
if (legacyDisposer !== null) {
|
|
13097
|
+
legacyDisposer();
|
|
13098
|
+
legacyDisposer = null;
|
|
13099
|
+
}
|
|
13100
|
+
const outcome = installOfficial({
|
|
13101
|
+
tabs: official.tabs,
|
|
13102
|
+
service: official.service,
|
|
13103
|
+
slots: ctx.slots,
|
|
13104
|
+
renderTab: officialRenderTab,
|
|
13105
|
+
guide: {
|
|
13106
|
+
title: () => OFFICIAL_TAB_TITLE,
|
|
13107
|
+
description: () => "Monaco 文件编辑器:对话与编辑同屏(差异审查/跳转/大纲)"
|
|
13108
|
+
}
|
|
13109
|
+
});
|
|
13110
|
+
if (outcome === null) {
|
|
13111
|
+
officialService = void 0;
|
|
13112
|
+
applySideForm();
|
|
13113
|
+
return;
|
|
13114
|
+
}
|
|
13115
|
+
officialDisposer = ctx.effect(() => outcome, "vscode-mode: official sidebar editor tab");
|
|
13116
|
+
if (typeof official.service.openResource === "function") ctx.effect(() => registry.register(officialSidebarOpener(official.service)), "vscode-mode: official sidebar opener");
|
|
13117
|
+
syncFileClaim();
|
|
13118
|
+
};
|
|
12447
13119
|
const applySideForm = () => {
|
|
13120
|
+
if (officialService) {
|
|
13121
|
+
installOfficialForm();
|
|
13122
|
+
return;
|
|
13123
|
+
}
|
|
12448
13124
|
if (sideService) {
|
|
12449
13125
|
if (sideDisposer !== null) return;
|
|
13126
|
+
if (officialDisposer !== null) {
|
|
13127
|
+
officialDisposer();
|
|
13128
|
+
officialDisposer = null;
|
|
13129
|
+
}
|
|
12450
13130
|
if (legacyDisposer !== null) {
|
|
12451
13131
|
legacyDisposer();
|
|
12452
13132
|
legacyDisposer = null;
|
|
@@ -12472,28 +13152,41 @@ window.__ModuleLoader__.load({
|
|
|
12472
13152
|
},
|
|
12473
13153
|
registerLegacyFallback: registerLegacyTab
|
|
12474
13154
|
}), "vscode-mode: sidebar editor tab");
|
|
12475
|
-
|
|
12476
|
-
if (legacyDisposer !== null) return;
|
|
12477
|
-
if (sideDisposer !== null) {
|
|
12478
|
-
sideDisposer();
|
|
12479
|
-
sideDisposer = null;
|
|
12480
|
-
}
|
|
12481
|
-
setEnsureSideEditor(null);
|
|
12482
|
-
legacyDisposer = registerLegacyTab();
|
|
13155
|
+
return;
|
|
12483
13156
|
}
|
|
13157
|
+
if (legacyDisposer !== null) return;
|
|
13158
|
+
if (officialDisposer !== null) {
|
|
13159
|
+
officialDisposer();
|
|
13160
|
+
officialDisposer = null;
|
|
13161
|
+
}
|
|
13162
|
+
if (sideDisposer !== null) {
|
|
13163
|
+
sideDisposer();
|
|
13164
|
+
sideDisposer = null;
|
|
13165
|
+
}
|
|
13166
|
+
setEnsureSideEditor(null);
|
|
13167
|
+
legacyDisposer = registerLegacyTab();
|
|
12484
13168
|
};
|
|
12485
13169
|
applySideForm();
|
|
12486
13170
|
let sideRetries = 0;
|
|
12487
13171
|
const retrySideService = () => {
|
|
12488
|
-
if (
|
|
13172
|
+
if (officialService !== void 0 || sideRetries >= 15) return;
|
|
12489
13173
|
sideRetries += 1;
|
|
12490
13174
|
schedule(() => {
|
|
12491
|
-
if (
|
|
12492
|
-
|
|
12493
|
-
if (
|
|
12494
|
-
log.info("
|
|
13175
|
+
if (officialService !== void 0) return;
|
|
13176
|
+
officialService = detectOfficial(ctx);
|
|
13177
|
+
if (officialService !== void 0) {
|
|
13178
|
+
log.info("检测到官方右侧 Sidebar(DSH 0.1.5+),切换官方侧边栏编辑形态");
|
|
12495
13179
|
applySideForm();
|
|
12496
|
-
|
|
13180
|
+
return;
|
|
13181
|
+
}
|
|
13182
|
+
if (sideService === void 0) {
|
|
13183
|
+
sideService = detectSidebarService(ctx);
|
|
13184
|
+
if (sideService !== void 0) {
|
|
13185
|
+
log.info("检测到 dsh-better-sidebar,切换侧边栏编辑形态(官方未探测到,后续仍优先官方)");
|
|
13186
|
+
applySideForm();
|
|
13187
|
+
}
|
|
13188
|
+
}
|
|
13189
|
+
retrySideService();
|
|
12497
13190
|
}, 2e3);
|
|
12498
13191
|
};
|
|
12499
13192
|
retrySideService();
|