dsh-vscode-mode 0.5.0 → 0.5.2

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
@@ -6744,7 +6744,9 @@ window.__ModuleLoader__.load({
6744
6744
  /**
6745
6745
  * 订阅窗口会话广播(幂等):EditorView 是 sessionId 的权威来源,挂载与会话切换时广播。
6746
6746
  * 独立于服务订阅,避免服务形状变化时 LSP 整体失联。
6747
- * @author ddj 2026年09月11号
6747
+ *
6748
+ * 返回值是可重入的取消订阅函数;调用方应保存并在卸载时调用(否则重载后会累积监听)。
6749
+ * @author ddj 2026年09月11号 / 2026年09月18号
6748
6750
  * @returns 取消订阅函数
6749
6751
  */
6750
6752
  function bindLspSession() {
@@ -6978,6 +6980,8 @@ window.__ModuleLoader__.load({
6978
6980
  };
6979
6981
  let registered$2 = false;
6980
6982
  const disposables = [];
6983
+ /** LSP provider 注册标记的全局挂点名(跨插件重载认领;见 disposeLspProviders)。 */
6984
+ const LSP_PROVIDERS_GLOBAL = "__edrvLspProvidersRegistered__";
6981
6985
  /** 目标文件打开并定位(复用现有 openFileAt 的 edrv:open-editor 事件通道)。 */
6982
6986
  function openAt(path, target) {
6983
6987
  window.dispatchEvent(new CustomEvent("edrv:open-editor", { detail: Object.assign({ path }, target) }));
@@ -7018,6 +7022,11 @@ window.__ModuleLoader__.load({
7018
7022
  /** 注册全部 Monaco LSP provider 与文档跟踪(幂等)。 */
7019
7023
  function registerLspProviders(monaco) {
7020
7024
  if (registered$2) return;
7025
+ const host = typeof window === "undefined" ? void 0 : window;
7026
+ if (host && host[LSP_PROVIDERS_GLOBAL]) {
7027
+ registered$2 = true;
7028
+ return;
7029
+ }
7021
7030
  registered$2 = true;
7022
7031
  const attachModel = (model) => {
7023
7032
  if (!model || model.uri.scheme !== "edrv") return;
@@ -7084,6 +7093,24 @@ window.__ModuleLoader__.load({
7084
7093
  },
7085
7094
  releaseDocumentSemanticTokens: () => {}
7086
7095
  }));
7096
+ if (host) host[LSP_PROVIDERS_GLOBAL] = true;
7097
+ }
7098
+ /**
7099
+ * 卸载 LSP provider 集合:注销全部注册并复位状态(插件重载/卸载时调用)。
7100
+ *
7101
+ * 必须存在的原因:DSH 0.1.6-alpha.2 起支持插件运行时卸载/重载,而 `window.monaco`
7102
+ * 由 loader 注入后**跨重载存活**;模块级 `registered`/`disposables` 却随 bundle 重新
7103
+ * 求值清空 —— 不注销则每次重载都重复叠加一整套 provider(跳转/hover/语义高亮翻倍)。
7104
+ * @author ddj 2026年09月18号
7105
+ */
7106
+ function disposeLspProviders() {
7107
+ for (const dispose of disposables.splice(0)) try {
7108
+ if (typeof dispose === "function") dispose();
7109
+ else if (dispose && typeof dispose.dispose === "function") dispose.dispose();
7110
+ } catch {}
7111
+ registered$2 = false;
7112
+ const host = typeof window === "undefined" ? void 0 : window;
7113
+ if (host) delete host[LSP_PROVIDERS_GLOBAL];
7087
7114
  }
7088
7115
  /** LSP SymbolInfo(host 归一化后)→ Monaco DocumentSymbol。 */
7089
7116
  function toMonacoSymbol(symbol) {
@@ -7270,12 +7297,23 @@ window.__ModuleLoader__.load({
7270
7297
  * 作者 ddj 2026-08-27
7271
7298
  */
7272
7299
  let sessionBound = false;
7300
+ /** 会话广播取消订阅函数(卸载时调用;落 window 以便跨重载清理上一代监听)。 */
7301
+ let sessionUnbind = null;
7302
+ /** 会话广播订阅标记的全局挂点名(跨插件重载认领;见 disposeLsp)。 */
7303
+ const LSP_SESSION_GLOBAL = "__edrvLspSessionUnbind__";
7273
7304
  /** Monaco 加载后装配(幂等;重复调用仅刷新会话)。 */
7274
7305
  function setupLsp(monaco) {
7275
7306
  registerLspProviders(monaco);
7307
+ const host = typeof window === "undefined" ? void 0 : window;
7308
+ if (host && typeof host[LSP_SESSION_GLOBAL] === "function") {
7309
+ sessionBound = true;
7310
+ sessionUnbind = host[LSP_SESSION_GLOBAL];
7311
+ return;
7312
+ }
7276
7313
  if (!sessionBound) {
7277
7314
  sessionBound = true;
7278
- bindLspSession();
7315
+ sessionUnbind = bindLspSession();
7316
+ if (host) host[LSP_SESSION_GLOBAL] = sessionUnbind;
7279
7317
  }
7280
7318
  }
7281
7319
  /** 会话切换:更新 host 侧文档归属 + 刷新状态。 */
@@ -7283,6 +7321,21 @@ window.__ModuleLoader__.load({
7283
7321
  setLspSession(sessionId);
7284
7322
  refreshStatus(true);
7285
7323
  }
7324
+ /**
7325
+ * 卸载 LSP 装配(插件重载/卸载时调用):注销全部 provider、解绑会话广播并复位标记。
7326
+ * @author ddj 2026年09月18号
7327
+ */
7328
+ function disposeLsp() {
7329
+ disposeLspProviders();
7330
+ const unbind = sessionUnbind;
7331
+ if (typeof unbind === "function") try {
7332
+ unbind();
7333
+ } catch {}
7334
+ sessionUnbind = null;
7335
+ sessionBound = false;
7336
+ const host = typeof window === "undefined" ? void 0 : window;
7337
+ if (host) delete host[LSP_SESSION_GLOBAL];
7338
+ }
7286
7339
  //#endregion
7287
7340
  //#region src/shared/ai.ts
7288
7341
  /**
@@ -7294,6 +7347,29 @@ window.__ModuleLoader__.load({
7294
7347
  /** 上下文窗口:前缀字符上限(含未保存编辑的当前文档)。 */
7295
7348
  const AI_PREFIX_MAX = 2e3;
7296
7349
  /**
7350
+ * 已从官方默认模型列表下架的模型标识(依据 DSH 0.1.6-alpha.2 release notes:
7351
+ * 「默认模型列表移除 V4 Flash 和 V4 Flash Vision Exp」)。
7352
+ *
7353
+ * 用途:用户此前保存过这些模型时,配置仍指向它们,而新版模型目录不再列出 →
7354
+ * 需给出可读降级提示(而非静默失败)。按模型名小写子串匹配,
7355
+ * 因为 provider 前缀与具体版本后缀(如 -exp)在不同部署下写法不一。
7356
+ */
7357
+ const DELISTED_MODEL_MARKERS = ["v4-flash"];
7358
+ /**
7359
+ * 配置指向的模型是否已被官方下架(G7)。
7360
+ *
7361
+ * 语义:仅当确实指定了 model(非空 = 非自动路由)且命中下架标识时返回 true。
7362
+ * 空 model 表示「自动取首个可用模型」,不构成问题,故不提示。
7363
+ * @author ddj 2026年09月18号
7364
+ * @param model 配置中的模型标识(可空)
7365
+ * @returns 是否已下架
7366
+ */
7367
+ function isDelistedModel(model) {
7368
+ const text = typeof model === "string" ? model.trim().toLowerCase() : "";
7369
+ if (!text) return false;
7370
+ return DELISTED_MODEL_MARKERS.some((marker) => text.includes(marker));
7371
+ }
7372
+ /**
7297
7373
  * 按 AI_PREFIX_MAX/AI_SUFFIX_MAX 裁剪前后缀(client 侧发请求前调用)。
7298
7374
  * @author ddj
7299
7375
  * @param prefix 光标前全文
@@ -7356,6 +7432,16 @@ window.__ModuleLoader__.load({
7356
7432
  let inFlight = null;
7357
7433
  let debounceTimer = null;
7358
7434
  const cache$1 = /* @__PURE__ */ new Map();
7435
+ /**
7436
+ * 补全 provider 注销器的全局挂点名。
7437
+ *
7438
+ * 为什么挂 window 而不是只靠模块级 registered:DSH 0.1.6-alpha.2 起支持插件运行时
7439
+ * 卸载/重载,而 `window.monaco` 由 loader 注入后**跨重载存活**,模块级状态却会随
7440
+ * bundle 重新求值而复位 —— 只判 registered 会在每次重载后再注册一次 provider,
7441
+ * 导致同一编辑器出现重复补全。故注销器落 window,重载时命中即跳过注册。
7442
+ * @author ddj 2026年09月18号
7443
+ */
7444
+ const AI_INLINE_GLOBAL = "__edrvAiInlineDisposer__";
7359
7445
  /** 上次开关状态(configUpdate 后经事件刷新,关闭时立即静默)。 */
7360
7446
  let enabled = false;
7361
7447
  /** 供设置面板保存后同步开关状态(不重载页面即时生效)。 */
@@ -7406,8 +7492,13 @@ window.__ModuleLoader__.load({
7406
7492
  */
7407
7493
  function registerAiInline(monaco) {
7408
7494
  if (registered$1 || !monaco?.languages?.registerInlineCompletionsProvider) return;
7495
+ const host = typeof window === "undefined" ? void 0 : window;
7496
+ if (host && host[AI_INLINE_GLOBAL]) {
7497
+ registered$1 = true;
7498
+ return;
7499
+ }
7409
7500
  registered$1 = true;
7410
- monaco.languages.registerInlineCompletionsProvider("*", {
7501
+ const inlineDisposer = monaco.languages.registerInlineCompletionsProvider("*", {
7411
7502
  async provideInlineCompletions(model, position, context, token) {
7412
7503
  const t0 = Date.now();
7413
7504
  if (!enabled || token?.isCancellationRequested) return { items: [] };
@@ -7493,6 +7584,36 @@ window.__ModuleLoader__.load({
7493
7584
  },
7494
7585
  freeInlineCompletions() {}
7495
7586
  });
7587
+ if (host) host[AI_INLINE_GLOBAL] = inlineDisposer;
7588
+ }
7589
+ /**
7590
+ * 卸载 AI 内联补全:注销 provider 并复位模块状态(插件重载/卸载时调用)。
7591
+ *
7592
+ * 复位 registered 让重装后能重新注册;清 window 挂点避免新实例被旧注销器误判为「已注册」。
7593
+ * 同时作废在飞请求与去抖定时器,防止卸载后仍有回调写状态。
7594
+ * @author ddj 2026年09月18号
7595
+ */
7596
+ function disposeAiInline() {
7597
+ const host = typeof window === "undefined" ? void 0 : window;
7598
+ const inlineDisposer = host ? host[AI_INLINE_GLOBAL] : void 0;
7599
+ if (inlineDisposer && typeof inlineDisposer.dispose === "function") try {
7600
+ inlineDisposer.dispose();
7601
+ } catch {}
7602
+ if (host) delete host[AI_INLINE_GLOBAL];
7603
+ registered$1 = false;
7604
+ seq += 1;
7605
+ if (debounceTimer) {
7606
+ clearTimeout(debounceTimer);
7607
+ debounceTimer = null;
7608
+ }
7609
+ if (inFlight) {
7610
+ try {
7611
+ inFlight.abort();
7612
+ } catch {}
7613
+ inFlight = null;
7614
+ }
7615
+ cache$1.clear();
7616
+ monacoRef = null;
7496
7617
  }
7497
7618
  /** 缓存写入(超限整体清空,LRU 简化)。 */
7498
7619
  function cacheSet(key, items) {
@@ -7582,6 +7703,16 @@ window.__ModuleLoader__.load({
7582
7703
  let cacheAt = 0;
7583
7704
  let pending = null;
7584
7705
  let registered = false;
7706
+ let disposer = null;
7707
+ /**
7708
+ * 片段 provider 注销器的全局挂点名。
7709
+ *
7710
+ * 为什么挂 window:`window.monaco` 由 loader 注入后**跨插件重载存活**,而模块级
7711
+ * registered/disposer 会随 bundle 重新求值复位 —— 只判 registered 会在每次重载后
7712
+ * 重复注册补全 provider(同一编辑器出现重复候补)。故注销器落 window 供跨代认领与清理。
7713
+ * @author ddj 2026年09月18号
7714
+ */
7715
+ const SNIPPET_GLOBAL = "__edrvSnippetsDisposer__";
7585
7716
  /** 当前会话 id(补全按会话工作区叠加项目片段;EditorView 装配时注入)。 */
7586
7717
  let sessionId = null;
7587
7718
  /**
@@ -7653,11 +7784,16 @@ window.__ModuleLoader__.load({
7653
7784
  */
7654
7785
  function registerSnippetProvider(monaco) {
7655
7786
  if (registered || !monaco?.languages?.registerCompletionItemProvider) return;
7787
+ const host = typeof window === "undefined" ? void 0 : window;
7788
+ if (host && host[SNIPPET_GLOBAL]) {
7789
+ registered = true;
7790
+ return;
7791
+ }
7656
7792
  const snippetKind = monaco.languages.CompletionItemKind?.Snippet;
7657
7793
  const asSnippet = monaco.languages.CompletionItemInsertTextRule?.InsertAsSnippet;
7658
7794
  if (typeof snippetKind !== "number" || typeof asSnippet !== "number") return;
7659
7795
  registered = true;
7660
- monaco.languages.registerCompletionItemProvider("*", { async provideCompletionItems(model, position) {
7796
+ disposer = monaco.languages.registerCompletionItemProvider("*", { async provideCompletionItems(model, position) {
7661
7797
  const entries = await loadEntries();
7662
7798
  if (!entries.length) return { suggestions: [] };
7663
7799
  const items = entriesForLanguage(entries, model?.getLanguageId?.());
@@ -7680,6 +7816,7 @@ window.__ModuleLoader__.load({
7680
7816
  range
7681
7817
  })).filter((item) => Boolean(item.label)) };
7682
7818
  } });
7819
+ if (host) host[SNIPPET_GLOBAL] = disposer;
7683
7820
  }
7684
7821
  /**
7685
7822
  * 装配:Monaco 就绪后注册 provider(幂等)。
@@ -7690,6 +7827,22 @@ window.__ModuleLoader__.load({
7690
7827
  registerSnippetProvider(monaco);
7691
7828
  }
7692
7829
  /**
7830
+ * 卸载:注销 provider 并复位状态(插件热重载/卸载时调用)。
7831
+ * 兼容上一代 bundle 遗留的 window 注销器(模块级 disposer 为空时仍能清干净)。
7832
+ * @author ddj 2026年09月10号 / 2026年09月18号
7833
+ */
7834
+ function disposeSnippets() {
7835
+ const host = typeof window === "undefined" ? void 0 : window;
7836
+ const target = (typeof disposer === "function" ? disposer : null) ?? (host ? host[SNIPPET_GLOBAL] : null);
7837
+ if (target && typeof target.dispose === "function") try {
7838
+ target.dispose();
7839
+ } catch {}
7840
+ if (host) delete host[SNIPPET_GLOBAL];
7841
+ disposer = null;
7842
+ registered = false;
7843
+ invalidateSnippets();
7844
+ }
7845
+ /**
7693
7846
  * 读取可插入条目(「插入代码片段」命令用;含无前缀条目)。
7694
7847
  * @author ddj 2026年09月10号
7695
7848
  * @param languageId 当前模型语言(缺省返回全语言 + 该语言条目)
@@ -8258,23 +8411,50 @@ window.__ModuleLoader__.load({
8258
8411
  /**
8259
8412
  * 归一化持久化的页签数据:兼容旧版 `string[]`、去重、固定分区。
8260
8413
  * 损坏项(非字符串/非对象/无 path)直接丢弃。
8261
- * @author ddj 2026年09月11号
8414
+ *
8415
+ * G9:传入 cwd 时把每个路径收敛为**页签规范形态**(工作区相对路径),
8416
+ * 以迁移历史持久化数据里残留的绝对路径(见 {@link tabPathOf})。
8417
+ * @author ddj 2026年09月11号 / 2026年09月18号
8262
8418
  * @param raw localStorage 解析结果(任意形状)
8419
+ * @param cwd 会话工作区目录(可选;给出时同步归一化路径形态)
8263
8420
  * @returns 归一化后的页签数组
8264
8421
  */
8265
- function normalizeTabs(raw) {
8422
+ function normalizeTabs(raw, cwd) {
8266
8423
  if (!Array.isArray(raw)) return [];
8267
8424
  const seen = /* @__PURE__ */ new Set();
8268
8425
  const out = [];
8269
8426
  for (const item of raw) {
8270
8427
  const tab = tabOf(item);
8271
- if (!tab || seen.has(tab.path)) continue;
8272
- seen.add(tab.path);
8273
- out.push(tab);
8428
+ if (!tab) continue;
8429
+ const path = tabPathOf(tab.path, cwd);
8430
+ if (!path || seen.has(path)) continue;
8431
+ seen.add(path);
8432
+ out.push(tab.pinned === true ? {
8433
+ path,
8434
+ pinned: true
8435
+ } : { path });
8274
8436
  }
8275
8437
  return pinnedFirst(out);
8276
8438
  }
8277
8439
  /**
8440
+ * 页签规范形态 = 工作区相对路径(G9)。
8441
+ *
8442
+ * 为什么需要:差异记录路径取自工具结果 `target.displayPath`(官方恒为绝对拼写),
8443
+ * 而资源管理器树给出的是工作区相对路径。两者进页签后,地址栏形态不一致,且
8444
+ * `insertTab` 按原串去重 → 同一文件可能出两个页签;绝对路径还会被
8445
+ * `isTreeRevealable` 判为不可定位,「在资源管理器视图中显示」对差异入口失效。
8446
+ * 统一经 {@link relativeOf} 收敛后,页签/地址栏/去重/持久化/`sameFile` 口径一致。
8447
+ *
8448
+ * 工作区外文件(全局片段/规则)由 relativeOf 自然回退为原绝对路径,语义不变。
8449
+ * @author ddj 2026年09月18号
8450
+ * @param path 原始路径(绝对或相对,两种分隔符均可)
8451
+ * @param cwd 会话工作区目录(可空;无 cwd 时仅做分隔符归一)
8452
+ * @returns 页签规范路径
8453
+ */
8454
+ function tabPathOf(path, cwd) {
8455
+ return relativeOf(path, cwd);
8456
+ }
8457
+ /**
8278
8458
  * 解释单条持久化页签:字符串 = 路径(旧版),对象取 path/pinned。
8279
8459
  * @author ddj 2026年09月11号
8280
8460
  * @param item 原始条目
@@ -11410,6 +11590,32 @@ window.__ModuleLoader__.load({
11410
11590
  if (select) setActive(path);
11411
11591
  };
11412
11592
  /**
11593
+ * 页签规范路径(G9):统一收敛为工作区相对路径后再进页签。
11594
+ *
11595
+ * 差异记录路径取自工具结果 `target.displayPath`(官方恒绝对),而资源管理器树给相对路径;
11596
+ * 不归一会导致地址栏形态不一致、同文件出现两个页签(insertTab 按原串去重)、
11597
+ * 且绝对路径被 isTreeRevealable 判为不可定位(「在资源管理器视图中显示」失效)。
11598
+ * 工作区外文件由 relativeOf 回退原绝对路径,语义不变。
11599
+ * @author ddj 2026年09月18号
11600
+ * @param path 原始路径(绝对或相对)
11601
+ * @returns 页签规范路径
11602
+ */
11603
+ const tabPath = (path) => tabPathOf(path, cwd);
11604
+ /**
11605
+ * 打开文件入口的统一收敛(G9):所有「进页签」路径都经此归一,避免逐点打补丁。
11606
+ * @author ddj 2026年09月18号
11607
+ * @param path 原始路径
11608
+ * @param select 是否设为活动页签
11609
+ * @returns 归一后的路径(空值返回 null)
11610
+ */
11611
+ const addTabNorm = (path, select) => {
11612
+ if (!path) return null;
11613
+ const normalized = tabPath(path);
11614
+ if (!normalized) return null;
11615
+ addTab(normalized, select);
11616
+ return normalized;
11617
+ };
11618
+ /**
11413
11619
  * 保存当前活动文件的视图状态(光标/滚动/折叠)到工作区作用域缓存。
11414
11620
  * @author ddj 2026年08月28号
11415
11621
  * @param path 要保存的文件路径(缺省 = 当前 active)
@@ -11495,7 +11701,7 @@ window.__ModuleLoader__.load({
11495
11701
  flushSave();
11496
11702
  saveViewState(active);
11497
11703
  navPendingRef.current = entry;
11498
- addTab(entry.path, true);
11704
+ addTabNorm(entry.path, true);
11499
11705
  setFocusRequest((value) => value + 1);
11500
11706
  };
11501
11707
  /**
@@ -11978,22 +12184,24 @@ window.__ModuleLoader__.load({
11978
12184
  const onOpen = (e) => {
11979
12185
  const p = e?.detail?.path;
11980
12186
  if (!p) return;
12187
+ const normalized = tabPath(p);
12188
+ if (!normalized) return;
11981
12189
  if (e?.detail?.focusDiff === true) {
11982
12190
  recordNav();
11983
12191
  pendingFocusRef.current = {
11984
- path: p,
12192
+ path: normalized,
11985
12193
  region: null
11986
12194
  };
11987
12195
  setFocusRequest((value) => value + 1);
11988
- addTab(p, true);
12196
+ addTab(normalized, true);
11989
12197
  return;
11990
12198
  }
11991
12199
  if (e?.detail?.line != null) {
11992
- openFileAt(p, e?.detail?.line, e?.detail?.column, e?.detail?.endLine, e?.detail?.endColumn);
12200
+ openFileAt(normalized, e?.detail?.line, e?.detail?.column, e?.detail?.endLine, e?.detail?.endColumn);
11993
12201
  return;
11994
12202
  }
11995
12203
  recordNav();
11996
- addTab(p, true);
12204
+ addTab(normalized, true);
11997
12205
  };
11998
12206
  const onShowLauncher = (event) => {
11999
12207
  const tab = event?.detail?.tab;
@@ -12077,10 +12285,11 @@ window.__ModuleLoader__.load({
12077
12285
  const raw = localStorage.getItem(CACHE_KEY.editor + String(scope)) ?? localStorage.getItem(CACHE_KEY.editorLegacy + String(scope));
12078
12286
  if (raw) {
12079
12287
  const saved = JSON.parse(raw);
12080
- const restored = normalizeTabs(saved?.tabs);
12288
+ const restored = normalizeTabs(saved?.tabs, cwd);
12081
12289
  if (restored.length) {
12082
12290
  setTabs(restored);
12083
- setActive(pickActive(restored, saved?.active));
12291
+ const wanted = typeof saved?.active === "string" ? tabPathOf(saved.active, cwd) : saved?.active;
12292
+ setActive(pickActive(restored, wanted));
12084
12293
  }
12085
12294
  }
12086
12295
  } catch (e) {}
@@ -13818,9 +14027,9 @@ window.__ModuleLoader__.load({
13818
14027
  if (!path) return;
13819
14028
  setSvnDiff(null);
13820
14029
  recordNav();
13821
- addTab(path, true);
13822
- if (focusDiff) pendingFocusRef.current = {
13823
- path,
14030
+ const normalized = addTabNorm(path, true);
14031
+ if (focusDiff && normalized) pendingFocusRef.current = {
14032
+ path: normalized,
13824
14033
  region: null
13825
14034
  };
13826
14035
  };
@@ -13837,9 +14046,10 @@ window.__ModuleLoader__.load({
13837
14046
  const openFileAt = (path, line, column, endLine, endColumn) => {
13838
14047
  if (!path) return;
13839
14048
  recordNav();
13840
- addTab(path, true);
14049
+ const normalized = addTabNorm(path, true);
14050
+ if (!normalized) return;
13841
14051
  pendingFocusRef.current = {
13842
- path,
14052
+ path: normalized,
13843
14053
  region: null,
13844
14054
  line: line ?? null,
13845
14055
  column: column ?? 1,
@@ -14075,7 +14285,7 @@ window.__ModuleLoader__.load({
14075
14285
  onClick: keepLocal
14076
14286
  }, "保留本地"));
14077
14287
  };
14078
- const otherFiles = sum.pendingFiles.filter((f) => f.path !== active);
14288
+ const otherFiles = sum.pendingFiles.filter((f) => !sameFile(f.path, active));
14079
14289
  /**
14080
14290
  * 渲染编辑器/文件加载进度面板。
14081
14291
  * @author ddj 2026年08月22号
@@ -14995,6 +15205,110 @@ window.__ModuleLoader__.load({
14995
15205
  /** file 资源类型的 kind(与页类型 edrvEditor 区分:同 kind 只允许一份 extension)。 */
14996
15206
  const OFFICIAL_FILE_KIND = "edrvEditorFile";
14997
15207
  /**
15208
+ * Office 文档后缀:官方 `dsh-client-ui-sidebar-documentpreview` 的 Office 渲染器
15209
+ * 声明 `doc/docx/xls/xlsx/ppt/pptx`,并在 alpha.2 起提供侧栏 Office 预览。
15210
+ * 本插件让位官方(不认领),否则会把这些文件路由进 Monaco 而丢掉 Office 预览。
15211
+ */
15212
+ const OFFICE_EXT = [
15213
+ "doc",
15214
+ "docx",
15215
+ "xls",
15216
+ "xlsx",
15217
+ "ppt",
15218
+ "pptx",
15219
+ "odt",
15220
+ "ods",
15221
+ "odp",
15222
+ "pages",
15223
+ "numbers"
15224
+ ];
15225
+ /**
15226
+ * 官方「不可预览二进制容器」清单(`UNVIEWABLE_BINARY_EXTENSIONS`,逐项取自
15227
+ * `dsh-client-ui-sidebar-documentpreview/lib/client.js` 的 `UNVIEWABLE_BINARY_EXTENSIONS`)。
15228
+ * 这些后缀官方会给出「无法预览」提示;本插件让位官方,避免把二进制当文本读成乱码。
15229
+ *
15230
+ * ⚠️ 刻意例外:官方清单含 `avif`,但本插件 {@link IMAGE_MIME} 支持 avif 图片预览
15231
+ * (浏览器原生解码),故从本表**移除** `avif` —— 保留本插件的图片预览优于官方的「不可预览」。
15232
+ * @author ddj 2026年09月18号
15233
+ */
15234
+ const BLIND_EXT = [
15235
+ "mp4",
15236
+ "mov",
15237
+ "avi",
15238
+ "mkv",
15239
+ "webm",
15240
+ "flv",
15241
+ "wmv",
15242
+ "m4v",
15243
+ "mp3",
15244
+ "wav",
15245
+ "flac",
15246
+ "ogg",
15247
+ "m4a",
15248
+ "aac",
15249
+ "wma",
15250
+ "opus",
15251
+ "zip",
15252
+ "gz",
15253
+ "tgz",
15254
+ "bz2",
15255
+ "xz",
15256
+ "zst",
15257
+ "7z",
15258
+ "rar",
15259
+ "tar",
15260
+ "jar",
15261
+ "exe",
15262
+ "dll",
15263
+ "so",
15264
+ "dylib",
15265
+ "bin",
15266
+ "o",
15267
+ "class",
15268
+ "pyc",
15269
+ "wasm",
15270
+ "ttf",
15271
+ "otf",
15272
+ "woff",
15273
+ "woff2",
15274
+ "eot",
15275
+ "dmg",
15276
+ "iso",
15277
+ "img",
15278
+ "sqlite",
15279
+ "db",
15280
+ "psd",
15281
+ "ai",
15282
+ "sketch",
15283
+ "tiff",
15284
+ "tif",
15285
+ "heic",
15286
+ "heif"
15287
+ ];
15288
+ /** 让位判定用后缀集合(模块级构建一次;小写、无前导点)。 */
15289
+ const DEFER_EXT_SET = /* @__PURE__ */ new Set([...OFFICE_EXT, ...BLIND_EXT]);
15290
+ /**
15291
+ * 取 path 的 basename 后缀(小写、无点);无后缀/隐藏文件返回 ''。
15292
+ * @author ddj 2026年09月18号
15293
+ * @param path 文件路径(`/` 或 `\` 分隔均可)
15294
+ * @returns 小写后缀或 ''
15295
+ */
15296
+ function suffixOf(path) {
15297
+ const base = String(path ?? "").replace(/\\/g, "/").split("/").pop() ?? "";
15298
+ const dot = base.lastIndexOf(".");
15299
+ return dot > 0 ? base.slice(dot + 1).toLowerCase() : "";
15300
+ }
15301
+ /**
15302
+ * 该路径是否应让位官方查看器(Office 文档 + 官方不可预览的二进制容器)。
15303
+ * 命中即不认领 `dsh-resource://file/**`,回落官方预览/提示。
15304
+ * @author ddj 2026年09月18号
15305
+ * @param path 文件路径(工作区相对或绝对)
15306
+ * @returns 是否让位官方
15307
+ */
15308
+ function deferToOfficial(path) {
15309
+ return DEFER_EXT_SET.has(suffixOf(path));
15310
+ }
15311
+ /**
14998
15312
  * 结构化探测官方右侧 Sidebar 服务(可选依赖:缺失/降级返回 undefined)。
14999
15313
  * 服务仅存在于 DSH 0.1.5-alpha.1+,探测命中 ≡ 版本判定成立。
15000
15314
  * @author ddj 2026年09月09号
@@ -15208,7 +15522,10 @@ window.__ModuleLoader__.load({
15208
15522
  * 注册官方 file 资源地址认领(extension 档接管 `dsh-resource://file/**`,
15209
15523
  * 聊天文件链接/文件树打开改落本插件编辑器;注销后官方 textpreview 自动恢复)。
15210
15524
  * 认领与否由「文件链接使用工具」设置驱动(见 fileOpeners.shouldClaimFiles)。
15211
- * @author ddj 2026年09月09号
15525
+ *
15526
+ * 让位规则(DSH 0.1.6-alpha.2 起):Office 文档与官方「不可预览」二进制容器不认领,
15527
+ * 回落官方查看器(Office 侧栏预览 / 「无法预览」提示),见 {@link deferToOfficial}。
15528
+ * @author ddj 2026年09月09号 / 2026年09月18号
15212
15529
  * @param options 装配参数(探测命中的注册表 + slots + 正文组件)
15213
15530
  * @returns 卸载器(反注册类型与正文);类型注册抛错返回 null(回落官方查看器)
15214
15531
  */
@@ -15221,7 +15538,11 @@ window.__ModuleLoader__.load({
15221
15538
  kind: OFFICIAL_FILE_KIND,
15222
15539
  patterns: [OFFICIAL_FILE_PATTERN],
15223
15540
  priority: "extension",
15224
- canOpen: (address) => parseOfficialFileAddress(address) !== null,
15541
+ canOpen: (address) => {
15542
+ const parsed = parseOfficialFileAddress(address);
15543
+ if (parsed === null) return false;
15544
+ return !deferToOfficial(parsed.path);
15545
+ },
15225
15546
  title: (address) => officialFileTitle(address)
15226
15547
  });
15227
15548
  } catch (error) {
@@ -16793,7 +17114,8 @@ window.__ModuleLoader__.load({
16793
17114
  };
16794
17115
  if (!cfg) return react.default.createElement("div", { className: "vsm-mcp-empty" }, error || "正在读取 AI 补全配置…");
16795
17116
  const modelValue = cfg.provider && cfg.model ? cfg.provider + "/" + cfg.model : "";
16796
- return react.default.createElement("section", { className: "vsm-lsp-card" }, react.default.createElement("h3", null, "AI 自动补全(实验)"), react.default.createElement("p", { className: "vsm-lsp-note" }, "编辑停顿后由模型生成内联建议(ghost text),Tab 接受,Alt+\\ 手动触发。每次补全是一次模型调用;建议选择非推理模型,且思考强度选「跟随默认」或最低档以获得更快响应。"), react.default.createElement("div", { className: "vsm-lsp-row" }, react.default.createElement("button", {
17117
+ const delisted = isDelistedModel(cfg.model);
17118
+ return react.default.createElement("section", { className: "vsm-lsp-card" }, react.default.createElement("h3", null, "AI 自动补全(实验)"), react.default.createElement("p", { className: "vsm-lsp-note" }, "编辑停顿后由模型生成内联建议(ghost text),Tab 接受,Alt+\\ 手动触发。每次补全是一次模型调用;建议选择非推理模型,且思考强度选「跟随默认」或最低档以获得更快响应。"), delisted ? react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, "当前模型「" + cfg.model + "」已从 DSH 默认模型列表移除(0.1.6-alpha.2 起)。该模型可能不再可用;将「模型」改回「自动」即可使用第一个可用模型(现有配置不会被自动改动)。") : null, react.default.createElement("div", { className: "vsm-lsp-row" }, react.default.createElement("button", {
16797
17119
  className: cfg.enabled ? "vsm-primary" : "",
16798
17120
  disabled: busy,
16799
17121
  onClick: toggle
@@ -21476,6 +21798,137 @@ window.__ModuleLoader__.load({
21476
21798
  };
21477
21799
  }
21478
21800
  //#endregion
21801
+ //#region src/client/sessionScope.ts
21802
+ /** 对象窄化(非对象返回 undefined,供后续安全取字段)。 */
21803
+ function asRecord(value) {
21804
+ return value && typeof value === "object" ? value : void 0;
21805
+ }
21806
+ /** 非空字符串取值(其他类型一律视为缺失)。 */
21807
+ function strOf(value) {
21808
+ return typeof value === "string" && value ? value : void 0;
21809
+ }
21810
+ /**
21811
+ * 从 `uiSession.current` 快照读出 sessionId。
21812
+ * 兼容两种形态:绑定源本身(`{ key }`)与其包裹形态(`{ value: { key } }`)——
21813
+ * 官方 `createBindingSource` 的 `getSnapshot()` 返回绑定值本身,但跨版本包裹层存在差异,
21814
+ * 故两者都尝试。
21815
+ * @author ddj 2026年09月18号
21816
+ * @param snapshot `uiSession.current` 的 `getSnapshot()` 结果(任意形状)
21817
+ * @returns sessionId,或 undefined
21818
+ */
21819
+ function uiSessionIdOf(snapshot) {
21820
+ const binding = asRecord(snapshot);
21821
+ if (!binding) return void 0;
21822
+ return strOf(binding.key) ?? strOf(asRecord(binding.value)?.key);
21823
+ }
21824
+ /**
21825
+ * 从 `sessions.list` 快照读出 sessionId。
21826
+ * 先取旧版 `current`;缺失时回退 `byId` 中 `retainedBy.mainView > 0` 的首行
21827
+ * (与官方 ui-session 的 publishMain 同判据,故新版行为与官方视图一致)。
21828
+ * @author ddj 2026年09月18号
21829
+ * @param list `sessions.list` 的 `getSnapshot()` 结果(任意形状)
21830
+ * @returns sessionId,或 undefined
21831
+ */
21832
+ function listSessionId(list) {
21833
+ const snap = asRecord(list);
21834
+ if (!snap) return void 0;
21835
+ const current = strOf(snap.current);
21836
+ if (current) return current;
21837
+ const byId = asRecord(snap.byId);
21838
+ if (!byId) return void 0;
21839
+ for (const [id, row] of Object.entries(byId)) {
21840
+ const mainView = asRecord(asRecord(row)?.retainedBy)?.mainView;
21841
+ if (typeof mainView === "number" && mainView > 0) return id;
21842
+ }
21843
+ }
21844
+ /**
21845
+ * 三级取值链(纯函数):决定当前会话 id 与其 cwd。
21846
+ * 优先级 uiSession > list.current > list.byId.retainedBy > fallback。
21847
+ * @author ddj 2026年09月18号
21848
+ * @param sources 候选来源集合
21849
+ * @returns 会话作用域(字段缺失即省略该键,不写 undefined 占位)
21850
+ */
21851
+ function pickSessionScope(sources) {
21852
+ const sessionId = uiSessionIdOf(sources.uiSession) ?? listSessionId(sources.list) ?? strOf(sources.fallbackSessionId);
21853
+ if (!sessionId) return {};
21854
+ const cwd = strOf(asRecord(asRecord(asRecord(sources.list)?.byId)?.[sessionId])?.cwd);
21855
+ return cwd ? {
21856
+ sessionId,
21857
+ cwd
21858
+ } : { sessionId };
21859
+ }
21860
+ /** 读 `ctx.get(name)`(缺失/异常返回 undefined,不抛错)。 */
21861
+ function safeGet(ctx, name) {
21862
+ try {
21863
+ return ctx.get(name);
21864
+ } catch {
21865
+ return;
21866
+ }
21867
+ }
21868
+ /** 取 `uiSession.current` 快照(服务或订阅面缺失时返回 undefined)。 */
21869
+ function uiCurrentSnapshot(ctx) {
21870
+ const current = asRecord(asRecord(safeGet(ctx, "uiSession"))?.current);
21871
+ const getSnapshot = current?.getSnapshot;
21872
+ if (typeof getSnapshot !== "function") return void 0;
21873
+ try {
21874
+ return getSnapshot.call(current);
21875
+ } catch {
21876
+ return;
21877
+ }
21878
+ }
21879
+ /** 取 `sessions.list` 快照(服务缺失时返回 undefined)。 */
21880
+ function listSnapshot(ctx) {
21881
+ const list = asRecord(asRecord(safeGet(ctx, "sessions"))?.list);
21882
+ const getSnapshot = list?.getSnapshot;
21883
+ if (typeof getSnapshot !== "function") return void 0;
21884
+ try {
21885
+ return getSnapshot.call(list);
21886
+ } catch {
21887
+ return;
21888
+ }
21889
+ }
21890
+ /**
21891
+ * 读当前会话作用域(服务面取值 + 三级链)。
21892
+ * @author ddj 2026年09月18号
21893
+ * @param ctx 客户端根上下文(只需 `get`)
21894
+ * @param fallbackSessionId 槽位注入的 sessionId(可选)
21895
+ * @returns 会话作用域
21896
+ */
21897
+ function readSessionScope(ctx, fallbackSessionId) {
21898
+ return pickSessionScope({
21899
+ uiSession: uiCurrentSnapshot(ctx),
21900
+ list: listSnapshot(ctx),
21901
+ fallbackSessionId
21902
+ });
21903
+ }
21904
+ /**
21905
+ * 订阅会话切换(同时挂 `uiSession.current` 与新/旧 `sessions.list`)。
21906
+ * 旧版无 `uiSession`,新版 `list` 不再变(切会话只动 uiSession),故两条都要挂;
21907
+ * 回调可能因两条源先后触发而重复,调用方按上一次 sessionId 自行去重。
21908
+ * @author ddj 2026年09月18号
21909
+ * @param ctx 客户端根上下文(只需 `get`)
21910
+ * @param listener 变更回调
21911
+ * @returns 取消订阅函数(幂等)
21912
+ */
21913
+ function subscribeScope(ctx, listener) {
21914
+ const disposers = [];
21915
+ const attach = (owner) => {
21916
+ const subscribe = asRecord(owner)?.subscribe;
21917
+ if (typeof subscribe !== "function") return;
21918
+ try {
21919
+ const disposer = subscribe.call(owner, listener);
21920
+ if (typeof disposer === "function") disposers.push(disposer);
21921
+ } catch {}
21922
+ };
21923
+ attach(asRecord(asRecord(safeGet(ctx, "uiSession"))?.current));
21924
+ attach(asRecord(asRecord(safeGet(ctx, "sessions"))?.list));
21925
+ return () => {
21926
+ for (const disposer of disposers.splice(0)) try {
21927
+ disposer();
21928
+ } catch {}
21929
+ };
21930
+ }
21931
+ //#endregion
21479
21932
  //#region src/client/index.ts
21480
21933
  /**
21481
21934
  * dsh-vscode-mode client — 浏览器半入口:slot 注册 + 装配。
@@ -21539,46 +21992,37 @@ window.__ModuleLoader__.load({
21539
21992
  const workspaces = ctx.get("workspaces");
21540
21993
  const sessions = ctx.get("sessions");
21541
21994
  setupExtOpen(ctx);
21542
- const monacoList = sessions?.list;
21543
- if (typeof window !== "undefined" && monacoList && typeof monacoList.subscribe === "function") {
21544
- const list = monacoList;
21995
+ if (typeof window !== "undefined") {
21545
21996
  const schedulePreload = () => {
21546
21997
  const preload = () => loadMonaco(() => {}).then((m) => setupLsp(m)).catch(() => {});
21547
21998
  if (typeof window.requestIdleCallback === "function") window.requestIdleCallback(preload, { timeout: 2e3 });
21548
21999
  else schedule(preload, 300);
21549
22000
  };
21550
- const onSessionEnter = () => {
21551
- const current = list.getSnapshot()?.current;
21552
- if (!current) return;
21553
- setSession(current);
22001
+ let preloadedFor;
22002
+ const onScopeChange = () => {
22003
+ const scope = readSessionScope(ctx);
22004
+ setSession(scope.sessionId);
22005
+ if (!scope.sessionId || scope.sessionId === preloadedFor) return;
22006
+ preloadedFor = scope.sessionId;
21554
22007
  schedulePreload();
21555
22008
  };
21556
- ctx.effect(() => list.subscribe(onSessionEnter), "vscode-mode: monaco session trigger");
21557
- onSessionEnter();
21558
- }
21559
- if (monacoList && typeof monacoList.subscribe === "function") {
21560
- const list = monacoList;
21561
- ctx.effect(() => list.subscribe(() => {
21562
- setSession(list.getSnapshot()?.current);
21563
- }), "vscode-mode: lsp session sync");
21564
- }
21565
- if (monacoList && typeof monacoList.subscribe === "function") {
21566
- const list = monacoList;
21567
- ctx.effect(() => {
21568
- let lastCurrent = list.getSnapshot?.()?.current;
21569
- const check = () => {
21570
- const current = list.getSnapshot?.()?.current;
21571
- if (!current || current === lastCurrent) return;
21572
- lastCurrent = current;
21573
- if (!isEditorTabActive() || officialService === void 0) return;
21574
- try {
21575
- officialService.service.openTabIn?.(current, OFFICIAL_TAB_KIND, {});
21576
- } catch {}
21577
- restoreEditorTab(officialService.service, schedule);
21578
- };
21579
- return list.subscribe(() => schedule(check, 0));
21580
- }, "vscode-mode: editor tab restore");
22009
+ ctx.effect(() => subscribeScope(ctx, onScopeChange), "vscode-mode: monaco session trigger");
22010
+ onScopeChange();
21581
22011
  }
22012
+ ctx.effect(() => {
22013
+ let lastCurrent = readSessionScope(ctx).sessionId;
22014
+ const check = () => {
22015
+ const current = readSessionScope(ctx).sessionId;
22016
+ if (!current || current === lastCurrent) return;
22017
+ lastCurrent = current;
22018
+ if (!isEditorTabActive() || officialService === void 0) return;
22019
+ try {
22020
+ officialService.service.openTabIn?.(current, OFFICIAL_TAB_KIND, {});
22021
+ } catch {}
22022
+ restoreEditorTab(officialService.service, schedule);
22023
+ };
22024
+ return subscribeScope(ctx, () => schedule(check, 0));
22025
+ }, "vscode-mode: editor tab restore");
21582
22026
  const originalOpenPath = workspaces?.openPath;
21583
22027
  const binder = pickSettingsBinder(ctx);
21584
22028
  const settings = binder.scope;
@@ -21587,15 +22031,8 @@ window.__ModuleLoader__.load({
21587
22031
  let remoteOpenInstalled = false;
21588
22032
  /** 两条文件链接路由共用的路由日志(openPathRouter / remoteOpenRouter;统一走插件日志器)。 */
21589
22033
  const routeLogger = (message) => log.warn(message);
21590
- /** 两条路由共用的 FileOpenContext:当前会话 id 与工作区 cwd */
21591
- const openContext = () => {
21592
- const current = sessions?.list?.getSnapshot?.();
21593
- const sessionId = current?.current;
21594
- return {
21595
- sessionId,
21596
- cwd: (sessionId ? current?.byId?.[sessionId] : void 0)?.cwd
21597
- };
21598
- };
22034
+ /** 两条路由共用的 FileOpenContext:当前会话 id 与工作区 cwd(跨版本取值链)。 */
22035
+ const openContext = () => readSessionScope(ctx);
21599
22036
  /** 可选探测 betterSidebar 服务(不进 inject:缺失会让插件停靠等待,杀死回退路径)。 */
21600
22037
  let sideService = detectSidebarService(ctx);
21601
22038
  /** 可选探测官方右侧 Sidebar(DSH 0.1.5-alpha.1+;探测命中 ≡ 版本判定,不进 inject 同上)。 */
@@ -21849,12 +22286,11 @@ window.__ModuleLoader__.load({
21849
22286
  sessions
21850
22287
  })),
21851
22288
  activeSession: () => {
21852
- const snapshot = sessions?.list?.getSnapshot?.();
21853
- const sessionId = snapshot?.current;
21854
- if (!sessionId) return void 0;
22289
+ const scope = readSessionScope(ctx);
22290
+ if (!scope.sessionId) return void 0;
21855
22291
  return {
21856
- sessionId,
21857
- cwd: snapshot?.byId?.[sessionId]?.cwd
22292
+ sessionId: scope.sessionId,
22293
+ cwd: scope.cwd
21858
22294
  };
21859
22295
  },
21860
22296
  registerLegacyFallback: registerLegacyTab
@@ -21925,6 +22361,17 @@ window.__ModuleLoader__.load({
21925
22361
  openerRegistry: registry,
21926
22362
  compatSummary
21927
22363
  })));
22364
+ ctx.effect(() => () => {
22365
+ try {
22366
+ disposeSnippets();
22367
+ } catch {}
22368
+ try {
22369
+ disposeAiInline();
22370
+ } catch {}
22371
+ try {
22372
+ disposeLsp();
22373
+ } catch {}
22374
+ }, "vscode-mode: monaco providers teardown");
21928
22375
  }
21929
22376
  //#endregion
21930
22377
  exports.apply = apply;