dsh-vscode-mode 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -460,25 +460,28 @@ DSH 每次启动监听随机端口,`http://127.0.0.1:<port>` 就是浏览器 o
460
460
 
461
461
  官方 `dsh plugin` 方式,三选一。下面以 `web` profile 为例;Desktop 或其他命名
462
462
  profile 请替换为实际目标,不要把示例目录当成当前运行 profile。升级前可阅读
463
- [0.16.0 发布说明](docs/release-0.16.0.md);从 0.14.2 或更旧版本升级还须阅读
463
+ [0.17.0 发布说明](docs/release-0.17.0.md);从 0.14.2 或更旧版本升级还须阅读
464
464
  [0.14.3 数据迁移与回退说明](docs/release-0.14.3.md)。
465
465
 
466
466
  ```bash
467
467
  # ① Git 安装(clone + prepare 构建;推荐打固定 tag)
468
- dsh plugin --profile web add github:Lenonss/DSH_VsCodeMode#v0.16.0
468
+ dsh plugin --profile web add github:Lenonss/DSH_VsCodeMode#v0.17.0
469
469
 
470
470
  # ② npm 注册表(发布到 npm 后)
471
- dsh plugin --profile web add dsh-vscode-mode@0.16.0
471
+ dsh plugin --profile web add dsh-vscode-mode@0.17.0
472
472
 
473
473
  # ③ GitHub Release tgz 直装(Release asset 可用后)
474
- dsh plugin --profile web add https://github.com/Lenonss/DSH_VsCodeMode/releases/download/v0.16.0/dsh-vscode-mode-0.16.0.tgz
474
+ dsh plugin --profile web add https://github.com/Lenonss/DSH_VsCodeMode/releases/download/v0.17.0/dsh-vscode-mode-0.17.0.tgz
475
475
  ```
476
476
 
477
- > **DSH 版本政策(0.14.10 起)**:六项 DSH peer 统一声明为 `*`,不再维护逐版
478
- > 白名单或版本上界。DSH 的 `includePrerelease` 检查接受 alpha、rc 与正式版;
479
- > npm 默认 semver 的 `*` 仍排除预发布版本,可能产生 peer 不匹配提示。
480
- > 运行时按实际能力检查,不再仅因版本较新告警;允许安装不代表未来版本已通过
481
- > 全功能验收。已发布包仍以其实际声明为准;npm 安装前应确认注册表中版本可查询。
477
+ > **DSH 版本政策(0.17.0 起)**:六项 DSH peer 仍统一声明为 `*`,另以
478
+ > `engines.dsh: ">=0.1.0-rc.1 <0.3.0-0"` 显式声明支持的 DSH 运行时区间——
479
+ > `engines.dsh` 一旦声明就**单独决定**兼容性(官方实现:`An own engines.dsh range
480
+ > decides alone and peerDependencies are ignored`),不满足时安装会被直接拒绝。
481
+ > 该区间接受 0.1.0-rc.1 起的全部 0.1.x / 0.2.x,拒绝 0.3.0 及以后的主版本;
482
+ > DSH 自身以 `includePrerelease` 求值,故 alpha、rc 与正式版同等对待。
483
+ > 运行时仍按实际能力检查;允许安装不代表该版本已通过全功能验收。
484
+ > 已发布包仍以其实际声明为准;npm 安装前应确认注册表中版本可查询。
482
485
  > 替换已有包后需重启目标 DSH Host 并强制刷新原页面;仅刷新不会更新 Host。
483
486
  > 从 0.14.2 或更旧版本升级时,首次访问工作区会执行 0.14.3 引入的 SQLite 活跃记录迁移。
484
487
  >
@@ -809,6 +812,7 @@ Keep All / Undo All 却是亮的。修复:单文件 Keep / Undo 覆盖冲突
809
812
  完整变更见 [GitHub Releases](https://github.com/Lenonss/DSH_VsCodeMode/releases)。
810
813
  近期关键版本:
811
814
 
815
+ - **v0.17.0**:**DSH 0.2.1 吸收与运行期优化**——① 修复插件元数据一直失效:`locale/*.json` 改 `{"meta":{...}}` 结构并导出 `./locale/*.json`,插件管理器标题与描述不再回退 `package.json.name`;② 新增 `engines.dsh: ">=0.1.0-rc.1 <0.3.0-0"` 显式声明支持区间(此前仅靠官方 `includePrerelease` 宽容);③ 跟随会话内 `cd`:13 处 `header.cwd` 直读改走 `workingDirectory` 投影,服务缺失/抛错时回落原始目录;④ 轮询收敛:差异角标 / 输入区 dock / 编辑器 / 体积角标原本各自 5s 拉一次 `edrv.list`,改为会话级共享订阅(单轮询 + 退订到 0 才释放);⑤ 界面壳层接入 `ctx.locale` 中英双语(slot 名与官方页签标题随语言切换);⑥ 新增插件管理器详情页「VSCodeMode」状态段。详见 [0.17.0 发布说明](docs/release-0.17.0.md)。
812
816
  - **v0.16.0**:**C# 智能体体验增强**——C# provider DotRush 优先于 ms-Roslyn(自带 sln 加载与反编译跳转;Roslyn 无 C# Dev Kit 时不加载项目);csharp 握手后自动找工作区根 `.sln` 发 Roslyn 专有 `solution/open`,元数据符号(Unity 引擎/NuGet 程序集)可跳转与 hover;DAP 调试支持 `runtime='dotnet'` 适配器(DotRush monodbg 形态),按 dll runtimeconfig 要求的运行时大版本选择 dotnet。
813
817
  - **v0.15.9**:**插件自我更新与可用更新提醒**——设置页「文件与安装」新增更新行:显示当前版本,一键检查 npm 上的最新版本,发现新版后可确认执行自我更新(host 端先卸载再安装插件包,安装失败自动恢复 profile package.json/pnpm-lock.yaml,更新期间页面会短暂断开)。客户端启动后延迟探测新版本,发现可用更新时弹 toast 提醒(每个版本只提醒一次),devForm 开发形态不提醒。详见 [0.15.9 发布说明](docs/release-0.15.9.md)。
814
818
  - **v0.15.8**:**跨端「会话不存在」兼容**——桌面端与 Web 端各自运行独立 DSH 实例、会话列表互通但会话仅在一端激活时,另一端文件编辑器不再卡在「文件加载失败:会话不存在 0%」:中央区显示「会话在当前端不可用」终态提示(说明成因、保留重试),资源管理器红条同样替换为友好文案。host 三处会话前置错误文案收敛为 shared 常量,client 按常量识别归类。详见 [0.15.8 发布说明](docs/release-0.15.8.md)。
package/lib/client.js CHANGED
@@ -3019,6 +3019,167 @@ window.__ModuleLoader__.load({
3019
3019
  } catch (e) {}
3020
3020
  }
3021
3021
  //#endregion
3022
+ //#region src/client/state/recordsFeed.ts
3023
+ /** 兜底轮询周期(毫秒):仅作事件丢失时的兜底,正常由 edrv:refresh 驱动。 */
3024
+ const FEED_INTERVAL_MS = 5e3;
3025
+ const feeds = /* @__PURE__ */ new Map();
3026
+ /** 取全局 window(Node 测试环境无 window)。 */
3027
+ function win() {
3028
+ return typeof window === "undefined" ? void 0 : window;
3029
+ }
3030
+ /**
3031
+ * 取或建会话订阅槽位。
3032
+ * @author ddj 2026年10月19号
3033
+ * @param sessionId 会话 id
3034
+ * @returns 订阅槽位
3035
+ */
3036
+ function entryOf(sessionId) {
3037
+ let entry = feeds.get(sessionId);
3038
+ if (!entry) {
3039
+ entry = {
3040
+ records: null,
3041
+ listeners: /* @__PURE__ */ new Set(),
3042
+ tickers: /* @__PURE__ */ new Set(),
3043
+ errorListeners: /* @__PURE__ */ new Set(),
3044
+ timer: null,
3045
+ onRefresh: null,
3046
+ seq: 0,
3047
+ inflight: false
3048
+ };
3049
+ feeds.set(sessionId, entry);
3050
+ }
3051
+ return entry;
3052
+ }
3053
+ /**
3054
+ * 把最新记录分发给订阅者(快照相等时也分发,交由消费方自行比对)。
3055
+ * @author ddj 2026年10月19号
3056
+ * @param entry 订阅槽位
3057
+ */
3058
+ function notifyRecords(entry) {
3059
+ const snapshot = entry.records;
3060
+ if (!snapshot) return;
3061
+ for (const listener of Array.from(entry.listeners)) try {
3062
+ listener(snapshot);
3063
+ } catch {}
3064
+ }
3065
+ /**
3066
+ * 拉取一次该会话的差异记录。
3067
+ * @author ddj 2026年10月19号
3068
+ * @param sessionId 会话 id
3069
+ * @param skipStale 是否跳过过期判定(与 edrv.list 参数同名同义)
3070
+ * @returns 拉取完成 Promise
3071
+ */
3072
+ function refreshFeed(sessionId, skipStale = false) {
3073
+ const entry = entryOf(sessionId);
3074
+ const current = ++entry.seq;
3075
+ entry.inflight = true;
3076
+ const payload = { sessionId };
3077
+ if (skipStale) payload.skipStale = true;
3078
+ return rpc("edrv.list", payload).then((res) => {
3079
+ if (current !== entry.seq) return;
3080
+ if (!res || !res.ok || !Array.isArray(res.records)) return;
3081
+ entry.records = res.records;
3082
+ notifyRecords(entry);
3083
+ }).catch((e) => {
3084
+ if (current !== entry.seq) return;
3085
+ const message = String(e);
3086
+ for (const listener of Array.from(entry.errorListeners)) try {
3087
+ listener(message);
3088
+ } catch {}
3089
+ }).finally(() => {
3090
+ if (current === entry.seq) entry.inflight = false;
3091
+ });
3092
+ }
3093
+ /**
3094
+ * 确保该会话的定时器与 refresh 事件监听已就绪(首个订阅者到来时启动)。
3095
+ * @author ddj 2026年10月19号
3096
+ * @param sessionId 会话 id
3097
+ * @param entry 订阅槽位
3098
+ */
3099
+ function ensureTimer$1(sessionId, entry) {
3100
+ if (entry.timer === null) entry.timer = setInterval(() => {
3101
+ const tickers = Array.from(entry.tickers);
3102
+ for (const tick of tickers) try {
3103
+ tick();
3104
+ } catch {}
3105
+ if (entry.listeners.size > 0 || entry.errorListeners.size > 0) refreshFeed(sessionId);
3106
+ }, FEED_INTERVAL_MS);
3107
+ if (entry.onRefresh === null) {
3108
+ const handler = () => {
3109
+ refreshFeed(sessionId);
3110
+ };
3111
+ const target = win();
3112
+ if (target) target.addEventListener("edrv:refresh", handler);
3113
+ entry.onRefresh = handler;
3114
+ }
3115
+ }
3116
+ /**
3117
+ * 会话无任何订阅者时释放定时器、事件监听与缓存快照。
3118
+ * @author ddj 2026年10月19号
3119
+ * @param sessionId 会话 id
3120
+ * @param entry 订阅槽位
3121
+ */
3122
+ function releaseIfIdle(sessionId, entry) {
3123
+ if (entry.listeners.size || entry.tickers.size || entry.errorListeners.size) return;
3124
+ if (entry.timer !== null) clearInterval(entry.timer);
3125
+ if (entry.onRefresh !== null) {
3126
+ const target = win();
3127
+ if (target) target.removeEventListener("edrv:refresh", entry.onRefresh);
3128
+ }
3129
+ feeds.delete(sessionId);
3130
+ }
3131
+ /**
3132
+ * 订阅会话的差异记录。
3133
+ * 首个订阅者触发一次立即拉取;同会话多订阅者共享一次 RPC。
3134
+ * @author ddj 2026年10月19号
3135
+ * @param sessionId 会话 id
3136
+ * @param listener 记录变化回调
3137
+ * @returns 取消订阅函数
3138
+ */
3139
+ function subscribeRecords(sessionId, listener) {
3140
+ const entry = entryOf(sessionId);
3141
+ entry.listeners.add(listener);
3142
+ ensureTimer$1(sessionId, entry);
3143
+ if (entry.records) listener(entry.records);
3144
+ else if (!entry.inflight) refreshFeed(sessionId);
3145
+ return () => {
3146
+ entry.listeners.delete(listener);
3147
+ releaseIfIdle(sessionId, entry);
3148
+ };
3149
+ }
3150
+ /**
3151
+ * 订阅会话的周期心跳(复用同一定时器,不额外制造定时器)。
3152
+ * @author ddj 2026年10月19号
3153
+ * @param sessionId 会话 id
3154
+ * @param listener 心跳回调
3155
+ * @returns 取消订阅函数
3156
+ */
3157
+ function subscribeTick(sessionId, listener) {
3158
+ const entry = entryOf(sessionId);
3159
+ entry.tickers.add(listener);
3160
+ ensureTimer$1(sessionId, entry);
3161
+ return () => {
3162
+ entry.tickers.delete(listener);
3163
+ releaseIfIdle(sessionId, entry);
3164
+ };
3165
+ }
3166
+ /**
3167
+ * 订阅会话记录拉取失败(错误提示用;同样复用该会话的共享通道)。
3168
+ * @author ddj 2026年10月19号
3169
+ * @param sessionId 会话 id
3170
+ * @param listener 失败回调,参数为已字符串化的错误
3171
+ * @returns 取消订阅函数
3172
+ */
3173
+ function subscribeErrors(sessionId, listener) {
3174
+ const entry = entryOf(sessionId);
3175
+ entry.errorListeners.add(listener);
3176
+ ensureTimer$1(sessionId, entry);
3177
+ return () => {
3178
+ entry.errorListeners.delete(listener);
3179
+ releaseIfIdle(sessionId, entry);
3180
+ };
3181
+ }
3182
+ //#endregion
3022
3183
  //#region src/client/monaco/theme.ts
3023
3184
  /**
3024
3185
  * dsh-vscode-mode client — Monaco 主题:官方令牌跟随 + 现役双套回落。
@@ -17816,6 +17977,7 @@ window.__ModuleLoader__.load({
17816
17977
  */
17817
17978
  function installOfficial(options) {
17818
17979
  const { tabs, service, slots, renderTab, guide } = options;
17980
+ const titleOf = typeof options.title === "function" ? options.title : () => OFFICIAL_TAB_TITLE;
17819
17981
  const open = (path, focusDiff) => {
17820
17982
  try {
17821
17983
  service.openTab(OFFICIAL_TAB_KIND, { params: {
@@ -17834,7 +17996,7 @@ window.__ModuleLoader__.load({
17834
17996
  id: OFFICIAL_TAB_ID,
17835
17997
  kind: OFFICIAL_TAB_KIND,
17836
17998
  priority: "extension",
17837
- title: () => OFFICIAL_TAB_TITLE,
17999
+ title: () => titleOf(),
17838
18000
  guide: [{
17839
18001
  order: 20,
17840
18002
  title: guide.title,
@@ -20392,7 +20554,7 @@ window.__ModuleLoader__.load({
20392
20554
  * @param state 任务快照
20393
20555
  * @returns 可读的状态行
20394
20556
  */
20395
- function countText(state) {
20557
+ function countText$1(state) {
20396
20558
  if (!state) return "等待 SVN 任务…";
20397
20559
  const c = state.counts;
20398
20560
  return "合并 " + c.G + " · 新增 " + c.A + " · 删除 " + c.D + " · 更新 " + c.U + " · 冲突 " + c.C + (c.R || c.E ? " · 其他 " + (c.R + c.E) : "");
@@ -20792,7 +20954,7 @@ window.__ModuleLoader__.load({
20792
20954
  */
20793
20955
  function updateFoot(props, state, closable, finished, cancel, dismiss, confirmClose, cleaned, cleanup, remaining, jump, refresh) {
20794
20956
  const locked = updateLocked(state);
20795
- return react.default.createElement("footer", { className: "edrv-update-foot" }, react.default.createElement("span", { className: "edrv-update-counts" }, countText(state)), remaining > 0 ? react.default.createElement("button", {
20957
+ return react.default.createElement("footer", { className: "edrv-update-foot" }, react.default.createElement("span", { className: "edrv-update-counts" }, countText$1(state)), remaining > 0 ? react.default.createElement("button", {
20796
20958
  type: "button",
20797
20959
  className: "edrv-svn-act edrv-update-jump",
20798
20960
  onClick: jump
@@ -22875,31 +23037,36 @@ window.__ModuleLoader__.load({
22875
23037
  }
22876
23038
  return true;
22877
23039
  };
23040
+ /**
23041
+ * 把共享订阅拿到的记录落进组件状态(内容一致时保持引用不变,避免整树重渲染)。
23042
+ * @private @author ddj 2026年10月19号
23043
+ * @param list 差异记录数组
23044
+ */
23045
+ const applyRecords = (list) => {
23046
+ const trace = isDebug();
23047
+ const compareStart = trace ? performance.now() : 0;
23048
+ const map = {};
23049
+ for (const r of list) {
23050
+ if (r.archived === true) continue;
23051
+ map[r.callId] = r;
23052
+ }
23053
+ setRecords((prev) => {
23054
+ const equal = sameRecords(prev, map);
23055
+ if (trace) try {
23056
+ const compareMs = performance.now() - compareStart;
23057
+ if (compareMs > 20) dbg(sessionId, `[DEBUG diffList] count=${list.length} compareMs=${compareMs.toFixed(1)} changed=${!equal}`);
23058
+ } catch (e) {}
23059
+ return equal ? prev : map;
23060
+ });
23061
+ };
23062
+ /**
23063
+ * 主动刷新一次差异记录(走会话级共享通道,同会话多消费方共享一次 RPC)。
23064
+ * @private @author ddj 2026年10月19号
23065
+ * @param skipStale 是否跳过过期判定
23066
+ */
22878
23067
  const refreshRecords = (skipStale) => {
22879
23068
  if (!sessionId) return;
22880
- const trace = isDebug();
22881
- const start = trace ? performance.now() : 0;
22882
- rpc("edrv.list", {
22883
- sessionId,
22884
- ...skipStale ? { skipStale: true } : {}
22885
- }).then((res) => {
22886
- const received = trace ? performance.now() : 0;
22887
- if (!res || !res.ok || !Array.isArray(res.records)) return;
22888
- const map = {};
22889
- for (const r of res.records) {
22890
- if (r.archived === true) continue;
22891
- map[r.callId] = r;
22892
- }
22893
- setRecords((prev) => {
22894
- const compareStart = trace ? performance.now() : 0;
22895
- const equal = sameRecords(prev, map);
22896
- if (trace) try {
22897
- const compareMs = performance.now() - compareStart;
22898
- if (compareMs > 20 || received - start > 100) dbg(sessionId, `[DEBUG diffList] count=${res.records.length} skipStale=${skipStale === true} rpcMs=${(received - start).toFixed(1)} compareMs=${compareMs.toFixed(1)} changed=${!equal}`);
22899
- } catch (e) {}
22900
- return equal ? prev : map;
22901
- });
22902
- }).catch((e) => setError("list异常:" + String(e)));
23069
+ refreshFeed(sessionId, skipStale === true);
22903
23070
  };
22904
23071
  /**
22905
23072
  * Reject callbacks for a replaced file or loader generation.
@@ -23224,14 +23391,13 @@ window.__ModuleLoader__.load({
23224
23391
  });
23225
23392
  };
23226
23393
  react.default.useEffect(() => {
23227
- if (!sessionId) return;
23394
+ if (!sessionId) return void 0;
23395
+ const offRecords = subscribeRecords(sessionId, applyRecords);
23396
+ const offErrors = subscribeErrors(sessionId, (message) => setError("list异常:" + message));
23228
23397
  refreshRecords();
23229
- const t = setInterval(refreshRecords, 5e3);
23230
- const onRefresh = () => refreshRecords();
23231
- window.addEventListener("edrv:refresh", onRefresh);
23232
23398
  return () => {
23233
- clearInterval(t);
23234
- window.removeEventListener("edrv:refresh", onRefresh);
23399
+ offRecords();
23400
+ offErrors();
23235
23401
  };
23236
23402
  }, [sessionId]);
23237
23403
  /**
@@ -27313,34 +27479,24 @@ window.__ModuleLoader__.load({
27313
27479
  //#region src/client/ui/DiffBadge.ts
27314
27480
  /**
27315
27481
  * dsh-vscode-mode client — DiffBadge:header 差异角标(仅工作区有差异时显示)。
27316
- * 迁移自原 src/client/index.ts 的 DiffBadge,语义不改。
27482
+ * 轮询已收敛到 state/recordsFeed.ts 的会话级共享订阅,本组件不再自建定时器。
27317
27483
  * 作者 ddj 2026-08-20
27318
27484
  */
27319
27485
  /**
27320
- * 会话 header 差异角标:5s 轮询 edrv.list,有差异时显示数字徽章,点击打开编辑区并拉起 DiffLauncher。
27486
+ * 会话 header 差异角标:订阅会话差异记录,有差异时显示数字徽章,点击打开编辑区并拉起 DiffLauncher。
27321
27487
  */
27322
27488
  function DiffBadge(props) {
27323
27489
  const sessionId = props?.sessionId;
27324
27490
  const [summary, setSummary] = react.default.useState(null);
27325
- const seq = react.default.useRef(0);
27326
- const load = react.default.useCallback(() => {
27327
- if (!sessionId) return;
27328
- const s = ++seq.current;
27329
- rpc("edrv.list", { sessionId }).then((res) => {
27330
- if (s !== seq.current || !res || !res.ok || !Array.isArray(res.records)) return;
27331
- setSummary(summarize(res.records));
27332
- }).catch(() => {});
27333
- }, [sessionId]);
27334
27491
  react.default.useEffect(() => {
27335
- load();
27336
- const t = setInterval(load, 5e3);
27337
- const onRefresh = () => load();
27338
- window.addEventListener("edrv:refresh", onRefresh);
27339
- return () => {
27340
- clearInterval(t);
27341
- window.removeEventListener("edrv:refresh", onRefresh);
27342
- };
27343
- }, [load]);
27492
+ if (!sessionId) {
27493
+ setSummary(null);
27494
+ return;
27495
+ }
27496
+ return subscribeRecords(sessionId, (records) => {
27497
+ setSummary(summarize(records));
27498
+ });
27499
+ }, [sessionId]);
27344
27500
  const n = summary?.totalFiles ?? 0;
27345
27501
  if (!n) return null;
27346
27502
  const click = () => {
@@ -27394,7 +27550,8 @@ window.__ModuleLoader__.load({
27394
27550
  //#region src/client/ui/PerfSizeBadge.ts
27395
27551
  /**
27396
27552
  * dsh-vscode-mode client — PerfSizeBadge:对话 header 会话体积指示器。
27397
- * 5s 轮询当前会话持久化体积(host 仅 stat,近零成本),≥1MB 显示、超阈值变色,
27553
+ * 定时器复用 state/recordsFeed.ts 的会话级心跳(单会话只保留 1 个定时器),
27554
+ * 轮询当前会话持久化体积(host 仅 stat,近零成本),≥1MB 显示、超阈值变色,
27398
27555
  * 引导"一次任务一个短会话":会话过大时建议 /compact 或新开会话。
27399
27556
  * 作者 ddj 2026-09-02
27400
27557
  */
@@ -27428,12 +27585,10 @@ window.__ModuleLoader__.load({
27428
27585
  }).catch(() => {});
27429
27586
  }, [sessionId, sessions]);
27430
27587
  react.default.useEffect(() => {
27588
+ if (!sessionId) return void 0;
27431
27589
  load();
27432
- const t = setInterval(load, 5e3);
27433
- return () => {
27434
- clearInterval(t);
27435
- };
27436
- }, [load]);
27590
+ return subscribeTick(sessionId, load);
27591
+ }, [load, sessionId]);
27437
27592
  if (!size || size < SHOW_MIN) return null;
27438
27593
  const label = size >= 1048576 ? (size / 1048576).toFixed(1) + "MB" : Math.round(size / 1024) + "KB";
27439
27594
  const color = size >= HARD ? "#dc2626" : size >= SOFT ? "#d97706" : "var(--dsw-text-3,#6b7280)";
@@ -27460,6 +27615,7 @@ window.__ModuleLoader__.load({
27460
27615
  /**
27461
27616
  * dsh-vscode-mode client — 会话级差异 dock。
27462
27617
  * conversation.input.dock 是唯一实例:普通对话显示摘要,文件编辑页显示完整操作条。
27618
+ * 轮询已收敛到 state/recordsFeed.ts 的会话级共享订阅,本组件不再自建定时器。
27463
27619
  * 作者 ddj 2026-08-26
27464
27620
  */
27465
27621
  const nextIndexBySession = /* @__PURE__ */ new Map();
@@ -27473,25 +27629,15 @@ window.__ModuleLoader__.load({
27473
27629
  const sessionId = props?.sessionId;
27474
27630
  const [summary, setSummary] = react.default.useState(null);
27475
27631
  const [, setStoreRevision] = react.default.useState(0);
27476
- const seq = react.default.useRef(0);
27477
- const load = react.default.useCallback(() => {
27478
- if (!sessionId) return;
27479
- const current = ++seq.current;
27480
- rpc("edrv.list", { sessionId }).then((res) => {
27481
- if (current !== seq.current || !res || !res.ok || !Array.isArray(res.records)) return;
27482
- setSummary(summarize(res.records));
27483
- }).catch(() => {});
27484
- }, [sessionId]);
27485
27632
  react.default.useEffect(() => {
27486
- load();
27487
- const timer = setInterval(load, 5e3);
27488
- const onRefresh = () => load();
27489
- window.addEventListener("edrv:refresh", onRefresh);
27490
- return () => {
27491
- clearInterval(timer);
27492
- window.removeEventListener("edrv:refresh", onRefresh);
27493
- };
27494
- }, [load]);
27633
+ if (!sessionId) {
27634
+ setSummary(null);
27635
+ return;
27636
+ }
27637
+ return subscribeRecords(sessionId, (records) => {
27638
+ setSummary(summarize(records));
27639
+ });
27640
+ }, [sessionId]);
27495
27641
  react.default.useEffect(() => {
27496
27642
  if (!sessionId) return void 0;
27497
27643
  const sync = () => setStoreRevision((value) => value + 1);
@@ -30584,6 +30730,73 @@ window.__ModuleLoader__.load({
30584
30730
  }, "保存配置"))));
30585
30731
  }
30586
30732
  //#endregion
30733
+ //#region src/client/ui/PluginDetailSection.ts
30734
+ /**
30735
+ * dsh-vscode-mode client — 官方插件管理器详情页的只读状态段。
30736
+ * 挂在 DSH 0.2.1 新增的 `plugins.detail.section`(list / root)扩展位上,
30737
+ * 只在「本插件自己」的详情页渲染,其余插件页返回 null。
30738
+ * 作者 ddj 2026年10月19号
30739
+ */
30740
+ /** 本插件标识(与 package.json name / bundle 行 id 一致)。 */
30741
+ const SELF_ID = "dsh-vscode-mode";
30742
+ /**
30743
+ * 判断详情页 subject 是否指向本插件。
30744
+ * 官方有两种形状:官方内置项 `{kind:'item', id}`、安装包 `{kind:'bundle', pkg:{name}}`。
30745
+ * @author ddj 2026年10月19号
30746
+ * @param subject 详情页主题
30747
+ * @returns 是否为本插件
30748
+ */
30749
+ function isSelfSubject(subject) {
30750
+ if (!subject || typeof subject !== "object") return false;
30751
+ if (subject.kind === "item") return subject.id === SELF_ID;
30752
+ if (subject.kind === "bundle") return subject.pkg?.name === SELF_ID;
30753
+ return false;
30754
+ }
30755
+ /**
30756
+ * 把适配项列表压成「正常 / 未生效」计数文案。
30757
+ * @author ddj 2026年10月19号
30758
+ * @param list 适配项数组
30759
+ * @returns 计数文案
30760
+ */
30761
+ function countText(list) {
30762
+ const items = Array.isArray(list) ? list : [];
30763
+ return items.filter((item) => item?.active === true).length + " / " + items.length + " 项生效";
30764
+ }
30765
+ /**
30766
+ * 插件详情页的只读状态段。
30767
+ * @author ddj 2026年10月19号
30768
+ * @param props DSH 注入的 { subject }
30769
+ * @returns 状态段元素或 null(非本插件页)
30770
+ */
30771
+ function PluginDetailSection(props) {
30772
+ const subject = props?.subject;
30773
+ const isSelf = isSelfSubject(subject);
30774
+ const [report, setReport] = react.default.useState(null);
30775
+ react.default.useEffect(() => {
30776
+ if (!isSelf) return void 0;
30777
+ let alive = true;
30778
+ rpc("compat", {}).then((result) => {
30779
+ if (!alive || !result || !result.ok) return;
30780
+ setReport(result.report ?? null);
30781
+ }).catch(() => {});
30782
+ return () => {
30783
+ alive = false;
30784
+ };
30785
+ }, [isSelf]);
30786
+ if (!isSelf) return null;
30787
+ const row = (name, note) => react.default.createElement("div", {
30788
+ key: name,
30789
+ className: "vsm-compat-item ok"
30790
+ }, react.default.createElement("span", { className: "vsm-compat-name" }, name), react.default.createElement("span", { className: "vsm-compat-note" }, note));
30791
+ const versionText = report?.dshVersion ? "DSH " + report.dshVersion : "未探测到 DSH 版本";
30792
+ const pluginText = report?.pluginVersion ? "插件 " + report.pluginVersion : "插件版本未上报";
30793
+ const warnCount = Array.isArray(report?.warnings) ? report.warnings.length : 0;
30794
+ return react.default.createElement("section", {
30795
+ className: "vsm-panel",
30796
+ "data-edrv-plugin-status": "1"
30797
+ }, react.default.createElement("h3", { className: "vsm-panel-title" }, "VSCodeMode 运行状态"), react.default.createElement("div", { className: "vsm-panel-body" }, row("版本", versionText + " · " + pluginText), row("版本适配", countText(report?.adapters)), row("外部插件与服务", countText(report?.external)), row("护栏", countText(report?.guards)), row("兼容性警告", warnCount ? warnCount + " 条(设置页「兼容性」可查看详情)" : "未检测到")));
30798
+ }
30799
+ //#endregion
30587
30800
  //#region src/client/openPathRouter.ts
30588
30801
  /**
30589
30802
  * dsh-vscode-mode client — DSH 对话文件链接的统一打开路由。
@@ -37914,6 +38127,78 @@ window.__ModuleLoader__.load({
37914
38127
  };
37915
38128
  }
37916
38129
  //#endregion
38130
+ //#region src/client/localeClient.ts
38131
+ /**
38132
+ * dsh-vscode-mode client — UI 壳层文案的 locale 接入。
38133
+ * 此前 `locale` 写在 inject 里却零调用,界面文案全为硬编码中文;
38134
+ * 这里只覆盖壳层(slot label、编辑 Tab 标题、侧栏引导),编辑器内部大段文案不动。
38135
+ * 所有对外函数都对 locale 服务做可选探测:旧版 DSH 无该服务时回落中文词典。
38136
+ * 作者 ddj 2026年10月19号
38137
+ */
38138
+ /** locale 命名空间(与包名一致,避免与其他插件串词条)。 */
38139
+ const LOCALE_NS = "dsh-vscode-mode";
38140
+ /** 壳层文案词典(键名即 slot label / Tab 标题的稳定标识)。 */
38141
+ const LOCALE_DICTS = {
38142
+ zh: {
38143
+ "slot.fileEditor": "文件编辑",
38144
+ "slot.diff": "差异",
38145
+ "slot.sessionSize": "会话体积",
38146
+ "slot.settings": "VSCodeMode",
38147
+ "guide.title": "文件编辑",
38148
+ "guide.description": "Monaco 文件编辑器:对话与编辑同屏(差异审查/跳转/大纲)"
38149
+ },
38150
+ en: {
38151
+ "slot.fileEditor": "File editor",
38152
+ "slot.diff": "Diff",
38153
+ "slot.sessionSize": "Session size",
38154
+ "slot.settings": "VSCodeMode",
38155
+ "guide.title": "File editor",
38156
+ "guide.description": "Monaco file editor: chat and editing side by side (diff review, jump, outline)"
38157
+ }
38158
+ };
38159
+ /**
38160
+ * 取词条:先当前语言、再中文、最后键本身。
38161
+ * @author ddj 2026年10月19号
38162
+ * @param lang 语言 id
38163
+ * @param key 词条键
38164
+ * @returns 文案
38165
+ */
38166
+ function lookup(lang, key) {
38167
+ return LOCALE_DICTS[lang]?.[key] ?? LOCALE_DICTS["zh"]?.[key] ?? key;
38168
+ }
38169
+ /**
38170
+ * 生成不依赖 locale 服务的直读翻译函数(服务缺失/注册失败时使用)。
38171
+ * @author ddj 2026年10月19号
38172
+ * @param lang 语言 id
38173
+ * @returns 翻译函数
38174
+ */
38175
+ function directTranslate(lang = "zh") {
38176
+ return (key) => lookup(lang, key);
38177
+ }
38178
+ /**
38179
+ * 注册本插件词典并返回翻译函数。
38180
+ * 服务缺失、重复注册或 bind 抛错都不影响插件其余部分:一律回落直读词典。
38181
+ * @author ddj 2026年10月19号
38182
+ * @param ctx DSH 客户端上下文
38183
+ * @returns 翻译函数(读取时才解析当前语言,可直接放进 slot label thunk)
38184
+ */
38185
+ function installLocale(ctx) {
38186
+ let service;
38187
+ try {
38188
+ service = ctx.get("locale");
38189
+ } catch {}
38190
+ try {
38191
+ if (service && typeof service.register === "function") service.register(LOCALE_NS, LOCALE_DICTS);
38192
+ } catch {}
38193
+ try {
38194
+ if (service && typeof service.bind === "function") {
38195
+ const bound = service.bind(LOCALE_NS);
38196
+ if (typeof bound === "function") return (key) => bound(key);
38197
+ }
38198
+ } catch {}
38199
+ return directTranslate();
38200
+ }
38201
+ //#endregion
37917
38202
  //#region src/client/planBridge.ts
37918
38203
  /** Optional DSH plan integration; no provider or approval UI replacement. @author ddj 2026年10月08号 */
37919
38204
  const CLAIM_ID = "dsh-vscode-mode/plan";
@@ -38155,7 +38440,7 @@ window.__ModuleLoader__.load({
38155
38440
  clearPlanState();
38156
38441
  };
38157
38442
  }
38158
- createLogger();
38443
+ createLogger().child("debug");
38159
38444
  //#endregion
38160
38445
  //#region src/client/updateNotice.ts
38161
38446
  /**
@@ -38284,6 +38569,7 @@ window.__ModuleLoader__.load({
38284
38569
  */
38285
38570
  async function apply(ctx) {
38286
38571
  const schedule = (fn, ms) => ctx.timeout(fn, ms);
38572
+ const t = installLocale(ctx);
38287
38573
  /**
38288
38574
  * 无门控上报界面状态镜像事件(绕过客户端 edrv.debug 开关,直接落 host 诊断日志)。
38289
38575
  * 开关关闭时 dbg 直接 return,「重启后没恢复」这类问题会在客户端侧彻底无痕,故另开一条通路。
@@ -38637,7 +38923,7 @@ window.__ModuleLoader__.load({
38637
38923
  name: "conversation.view",
38638
38924
  id: "edrv-editor",
38639
38925
  order: 5,
38640
- label: "文件编辑",
38926
+ label: () => t("slot.fileEditor"),
38641
38927
  inject: (sessionId) => ({ sessionId })
38642
38928
  }, (props) => react.default.createElement(EditorView, Object.assign({}, props, {
38643
38929
  layout: "tab",
@@ -38670,9 +38956,10 @@ window.__ModuleLoader__.load({
38670
38956
  service: official.service,
38671
38957
  slots: ctx.slots,
38672
38958
  renderTab: officialRenderTab,
38959
+ title: () => t("slot.fileEditor"),
38673
38960
  guide: {
38674
- title: () => OFFICIAL_TAB_TITLE,
38675
- description: () => "Monaco 文件编辑器:对话与编辑同屏(差异审查/跳转/大纲)"
38961
+ title: () => t("guide.title"),
38962
+ description: () => t("guide.description")
38676
38963
  }
38677
38964
  });
38678
38965
  if (outcome === null) {
@@ -38786,31 +39073,36 @@ window.__ModuleLoader__.load({
38786
39073
  name: "conversation.input.dock",
38787
39074
  id: "edrv-diff-dock",
38788
39075
  order: 30,
38789
- label: "差异",
39076
+ label: () => t("slot.diff"),
38790
39077
  inject: (sessionId) => ({ sessionId })
38791
39078
  }, (props) => react.default.createElement(ConversationDiffDock, Object.assign({}, props)));
38792
39079
  registerSlotSafely(ctx, {
38793
39080
  name: "conversation.session.header.utilities",
38794
39081
  id: "edrv-diff-badge",
38795
39082
  order: 90,
38796
- label: "差异"
39083
+ label: () => t("slot.diff")
38797
39084
  }, (props) => react.default.createElement(DiffBadge, Object.assign({}, props)));
38798
39085
  registerSlotSafely(ctx, {
38799
39086
  name: "conversation.session.header.utilities",
38800
39087
  id: "edrv-perf-size",
38801
39088
  order: 89,
38802
- label: "会话体积"
39089
+ label: () => t("slot.sessionSize")
38803
39090
  }, (props) => react.default.createElement(PerfSizeBadge, Object.assign({}, props, { sessions })));
38804
39091
  registerSlotSafely(ctx, {
38805
39092
  name: "settings.section",
38806
39093
  id: "vscode-mode",
38807
39094
  order: 30,
38808
- label: "VSCodeMode"
39095
+ label: () => t("slot.settings")
38809
39096
  }, () => react.default.createElement(SettingsContext.Provider, { value: bridge.scope() }, react.default.createElement(McpSettings, {
38810
39097
  openerRegistry: registry,
38811
39098
  compatSummary,
38812
39099
  getShortcuts: () => detectShortcuts(ctx)
38813
39100
  })));
39101
+ registerSlotSafely(ctx, {
39102
+ name: "plugins.detail.section",
39103
+ id: "edrv-plugin-status",
39104
+ order: 40
39105
+ }, (props) => react.default.createElement(PluginDetailSection, Object.assign({}, props)));
38814
39106
  ctx.effect(() => () => {
38815
39107
  try {
38816
39108
  disposeSnippets();