dsh-vscode-mode 0.15.4 → 0.15.5

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
@@ -465,13 +465,13 @@ profile 请替换为实际目标,不要把示例目录当成当前运行 profi
465
465
 
466
466
  ```bash
467
467
  # ① Git 安装(clone + prepare 构建;推荐打固定 tag)
468
- dsh plugin --profile web add github:Lenonss/DSH_VsCodeMode#v0.15.4
468
+ dsh plugin --profile web add github:Lenonss/DSH_VsCodeMode#v0.15.5
469
469
 
470
470
  # ② npm 注册表(发布到 npm 后)
471
- dsh plugin --profile web add dsh-vscode-mode@0.15.4
471
+ dsh plugin --profile web add dsh-vscode-mode@0.15.5
472
472
 
473
473
  # ③ GitHub Release tgz 直装(Release asset 可用后)
474
- dsh plugin --profile web add https://github.com/Lenonss/DSH_VsCodeMode/releases/download/v0.15.4/dsh-vscode-mode-0.15.4.tgz
474
+ dsh plugin --profile web add https://github.com/Lenonss/DSH_VsCodeMode/releases/download/v0.15.5/dsh-vscode-mode-0.15.5.tgz
475
475
  ```
476
476
 
477
477
  > **DSH 版本政策(0.14.10 起)**:六项 DSH peer 统一声明为 `*`,不再维护逐版
@@ -809,6 +809,8 @@ Keep All / Undo All 却是亮的。修复:单文件 Keep / Undo 覆盖冲突
809
809
  完整变更见 [GitHub Releases](https://github.com/Lenonss/DSH_VsCodeMode/releases)。
810
810
  近期关键版本:
811
811
 
812
+ - **v0.15.5**:**浮窗性能与客户端适配**——拖动/缩放改为逐帧直写几何、结束单次提交,多窗口共享视口监听;SVN 日志行表 memo 与选中集优化,冲突状态读取按批 4 并发。LSP 去抖发送前惰性读取全文,隐藏页面心跳目标周期降为 15 秒;兼容性摘要按版本、安装事实与用户选择显示适用诊断,补充搜索刷新回归。保留 0.15.4 全部功能;本次真实 GUI 未重测,用户已接受此发布缺口,未安装/重启当前实例,详见 [0.15.5 发布说明](docs/release-0.15.5.md)。
813
+
812
814
  - **v0.15.4**:**SVN 非模态浮窗与独立文件对比**——更新、日志、统计、补丁、目录对比及智能整理采用与文件编辑相同的页面内浮窗,支持移动、调整尺寸、点击置顶与后台 DSH 交互。文件对比不再占据主编辑区,保留二进制/编码提示和来源任务返回逻辑;确认类弹窗保持模态。真实 GUI 交互尚未验收,用户已接受此发布缺口;资源管理器“目录不存在”原因未定位,本版不宣称修复该提示。
813
815
 
814
816
  - **v0.15.3**:**搜索结果强制刷新**——目标文件内容被改动后,工作区搜索不再残留过期匹配:内容变更(编辑器保存、外部改动或结构变更)会自动重新执行当前查询,同关键词重搜不再命中 60 秒结果缓存(`edrv.searchContent` 新增 `force` 选项,仅本次调用跳过缓存读取、仍写入新结果)。更新后需重启 DSH Host;真实 GUI 现场重测尚未执行,发布不代表现场验收完成,详见 [0.15.3 发布说明](docs/release-0.15.3.md)。
package/lib/client.js CHANGED
@@ -1258,32 +1258,38 @@ window.__ModuleLoader__.load({
1258
1258
  //#endregion
1259
1259
  //#region src/client/ui/FloatingWindow.ts
1260
1260
  /** Shared in-page window chrome, without a mask or focus trap. @author ddj 2026年10月14号 */
1261
- /** @private @author ddj 2026年10月14号 @param start Originating press. @param rect Initial bounds. @param sizing Resize mode. @param apply Commit bounds. @param done Gesture cleanup notification. @returns Gesture cancellation. */
1262
- function beginGesture(start, rect, sizing, apply, done) {
1261
+ /** @private @author ddj 2026年10月14号 @param start Originating press. @param rect Initial bounds. @param sizing Resize mode. @param draw Paint live bounds without a render. @param apply Commit bounds once. @param done Gesture cleanup notification. @returns Gesture cancellation. */
1262
+ function beginGesture(start, rect, sizing, draw, apply, done) {
1263
1263
  let frame = null;
1264
1264
  let next = rect;
1265
- /** @private @author ddj 2026年10月14号 Apply no more than once per frame. */
1265
+ let moved = false;
1266
+ /** @private @author ddj 2026年10月14号 Paint no more than once per frame. */
1266
1267
  function paint() {
1267
1268
  frame = null;
1268
- apply(next);
1269
+ draw(next);
1269
1270
  }
1270
1271
  /** @private @author ddj 2026年10月14号 @param event Pointer motion. Ignore unrelated pointers. */
1271
1272
  function move(event) {
1272
1273
  if (event.pointerId !== start.pointerId) return;
1273
1274
  event.preventDefault();
1275
+ moved = true;
1274
1276
  next = moveFloat(rect, event.clientX - start.clientX, event.clientY - start.clientY, sizing, window.innerWidth, window.innerHeight);
1275
1277
  if (frame === null) frame = requestAnimationFrame(paint);
1276
1278
  }
1277
- /** @private @author ddj 2026年10月14号 Remove listeners and commit pending geometry. */
1279
+ /** @private @author ddj 2026年10月14号 Remove listeners, then commit the settled geometry exactly once. */
1278
1280
  function stop() {
1279
1281
  if (frame !== null) {
1280
1282
  cancelAnimationFrame(frame);
1281
- paint();
1283
+ frame = null;
1282
1284
  }
1283
1285
  window.removeEventListener("pointermove", move, true);
1284
1286
  window.removeEventListener("pointerup", end, true);
1285
1287
  window.removeEventListener("pointercancel", end, true);
1286
1288
  window.removeEventListener("blur", stop);
1289
+ if (moved) {
1290
+ draw(next);
1291
+ apply(next);
1292
+ }
1287
1293
  done();
1288
1294
  }
1289
1295
  /** @private @author ddj 2026年10月14号 @param event Pointer release. Only end originating gesture. */
@@ -1296,6 +1302,31 @@ window.__ModuleLoader__.load({
1296
1302
  window.addEventListener("blur", stop);
1297
1303
  return stop;
1298
1304
  }
1305
+ const resizeSinks = /* @__PURE__ */ new Set();
1306
+ let resizeFrame = null;
1307
+ /** @private @author ddj 2026年10月14号 Re-clamp every window at most once per frame. */
1308
+ function onViewportResize() {
1309
+ if (resizeFrame !== null) return;
1310
+ resizeFrame = requestAnimationFrame(() => {
1311
+ resizeFrame = null;
1312
+ for (const sink of [...resizeSinks]) sink();
1313
+ });
1314
+ }
1315
+ /** @private @author ddj 2026年10月14号 @param sink Per-window re-clamp callback. @returns Unsubscribe. */
1316
+ function subscribeResize(sink) {
1317
+ if (resizeSinks.size === 0) window.addEventListener("resize", onViewportResize);
1318
+ resizeSinks.add(sink);
1319
+ return () => {
1320
+ resizeSinks.delete(sink);
1321
+ if (resizeSinks.size === 0) {
1322
+ window.removeEventListener("resize", onViewportResize);
1323
+ if (resizeFrame !== null) {
1324
+ cancelAnimationFrame(resizeFrame);
1325
+ resizeFrame = null;
1326
+ }
1327
+ }
1328
+ };
1329
+ }
1299
1330
  /** @public @author ddj 2026年10月14号 @param props Window contents and lifecycle callbacks. @returns Movable, resizable nonmodal portal. */
1300
1331
  function FloatingWindow(props) {
1301
1332
  const id = react.default.useId();
@@ -1306,29 +1337,40 @@ window.__ModuleLoader__.load({
1306
1337
  const current = react.default.useRef(props.rect ?? bounds);
1307
1338
  current.current = props.rect ?? bounds;
1308
1339
  const cleanup = react.default.useRef(null);
1340
+ const gesture = react.default.useRef(null);
1309
1341
  react.default.useSyncExternalStore(watchFloats, floatVersion, floatVersion);
1310
1342
  /** @private @author ddj 2026年10月14号 @param rect Next geometry. Notify controlled owner or retain local geometry. */
1311
1343
  function apply(rect) {
1312
1344
  if (latest.current.rect) latest.current.onBounds?.(rect);
1313
1345
  else setBounds(rect);
1314
1346
  }
1347
+ /** @private @author ddj 2026年10月14号 @param rect Live gesture geometry. Write the DOM and hold it until the commit render lands. */
1348
+ function draw(rect) {
1349
+ gesture.current = rect;
1350
+ const node = root.current;
1351
+ if (!node) return;
1352
+ node.style.left = rect.left + "px";
1353
+ node.style.top = rect.top + "px";
1354
+ node.style.width = rect.width + "px";
1355
+ node.style.height = rect.height + "px";
1356
+ }
1315
1357
  /** @private @author ddj 2026年10月14号 End any active gesture. */
1316
1358
  function cancel() {
1317
1359
  cleanup.current?.();
1318
1360
  cleanup.current = null;
1319
1361
  }
1320
- /** @private @author ddj 2026年10月14号 Re-clamp after viewport resize. */
1362
+ /** @private @author ddj 2026年10月14号 Re-clamp after viewport resize, preferring the gesture that just settled. */
1321
1363
  function resize() {
1322
1364
  cancel();
1323
- apply(clampFloat(current.current, window.innerWidth, window.innerHeight));
1365
+ apply(clampFloat(gesture.current ?? current.current, window.innerWidth, window.innerHeight));
1324
1366
  }
1325
1367
  react.default.useEffect(() => {
1326
1368
  raiseFloat(id);
1327
- window.addEventListener("resize", resize);
1369
+ const off = subscribeResize(resize);
1328
1370
  resize();
1329
1371
  return () => {
1372
+ off();
1330
1373
  cancel();
1331
- window.removeEventListener("resize", resize);
1332
1374
  dropFloat(id);
1333
1375
  };
1334
1376
  }, [id]);
@@ -1347,11 +1389,12 @@ window.__ModuleLoader__.load({
1347
1389
  if (event.button !== 0 || !event.isPrimary) return;
1348
1390
  cancel();
1349
1391
  event.preventDefault();
1350
- cleanup.current = beginGesture(event, current.current, sizing, apply, clear);
1392
+ cleanup.current = beginGesture(event, gesture.current ?? current.current, sizing, draw, apply, clear);
1351
1393
  }
1352
- /** @private @author ddj 2026年10月14号 Clear completed gesture reference. */
1394
+ /** @private @author ddj 2026年10月14号 Release the settled geometry once React has committed it. */
1353
1395
  function clear() {
1354
1396
  cleanup.current = null;
1397
+ gesture.current = null;
1355
1398
  }
1356
1399
  /** @private @author ddj 2026年10月14号 @param event Title press. Preserve title action buttons. */
1357
1400
  function moveStart(event) {
@@ -1372,7 +1415,8 @@ window.__ModuleLoader__.load({
1372
1415
  }
1373
1416
  props.onKeyDown?.(event);
1374
1417
  }
1375
- const rect = clampFloat(props.rect ?? bounds, window.innerWidth, window.innerHeight);
1418
+ const base = clampFloat(props.rect ?? bounds, window.innerWidth, window.innerHeight);
1419
+ const rect = gesture.current ?? base;
1376
1420
  return (0, react_dom.createPortal)(react.default.createElement("div", {
1377
1421
  ref: root,
1378
1422
  tabIndex: -1,
@@ -13177,19 +13221,21 @@ window.__ModuleLoader__.load({
13177
13221
  /**
13178
13222
  * 立即同步某文档到 host(首次打开/查询前保证服务器已认识该文档)。
13179
13223
  * sessionId 缺失时仍发起请求:host 对唯一活跃会话有兜底解析,弃发会白白丢失文档归属。
13180
- * @author ddj 2026年08月27号 / 2026年09月11号
13224
+ * @author ddj 2026年08月27号 / 2026年09月11号 / 2026年10月15号
13181
13225
  * @param path 工作区相对路径
13182
- * @param text 文档全文
13226
+ * @param text 文档全文;传函数表示**发送前才求值**(击键路径用它,避免为被去抖丢弃的中间态取全文)
13183
13227
  * @param immediate true 立即发送,false 去抖
13184
13228
  */
13185
13229
  async function syncDoc(path, text, immediate = false) {
13186
- if (!path || typeof text !== "string") return;
13230
+ if (!path || typeof text !== "string" && typeof text !== "function") return;
13187
13231
  if (immediate) {
13188
13232
  clearPending(path);
13233
+ const value = resolveText(text);
13234
+ if (value === null) return;
13189
13235
  await rpc("edrv.lsp.sync", {
13190
13236
  sessionId: sessionId$1,
13191
13237
  path,
13192
- text,
13238
+ text: value,
13193
13239
  version: nextDocVer(path)
13194
13240
  }).catch(() => {});
13195
13241
  return;
@@ -13197,14 +13243,32 @@ window.__ModuleLoader__.load({
13197
13243
  clearPending(path);
13198
13244
  syncTimers.set(path, setTimeout(() => {
13199
13245
  syncTimers.delete(path);
13246
+ const value = resolveText(text);
13247
+ if (value === null) return;
13200
13248
  rpc("edrv.lsp.sync", {
13201
13249
  sessionId: sessionId$1,
13202
13250
  path,
13203
- text,
13251
+ text: value,
13204
13252
  version: nextDocVer(path)
13205
13253
  }).catch(() => {});
13206
13254
  }, SYNC_DEBOUNCE_MS));
13207
13255
  }
13256
+ /**
13257
+ * 取本次要同步的文本:字符串原样返回,函数形态在发送前求值。
13258
+ * 取值抛错(model 已销毁)时返回 null 表示**放弃本次同步**——不能退回空串,
13259
+ * 那会把 host 侧文档内容清掉,比漏一次同步严重得多(关闭路径由 closeDoc 负责)。
13260
+ * @author ddj 2026年10月15号
13261
+ * @param text 字符串或惰性取值函数
13262
+ * @returns 待发送文本;无法取值时为 null
13263
+ */
13264
+ function resolveText(text) {
13265
+ if (typeof text === "string") return text;
13266
+ try {
13267
+ return String(text());
13268
+ } catch (error) {
13269
+ return null;
13270
+ }
13271
+ }
13208
13272
  function clearPending(path) {
13209
13273
  const timer = syncTimers.get(path);
13210
13274
  if (timer) {
@@ -13677,7 +13741,7 @@ window.__ModuleLoader__.load({
13677
13741
  if (!model || model.uri.scheme !== "edrv") return;
13678
13742
  const path = pathOfModel(model);
13679
13743
  if (!path) return;
13680
- const contentDisposer = model.onDidChangeContent(() => syncDoc(path, model.getValue()));
13744
+ const contentDisposer = model.onDidChangeContent(() => syncDoc(path, () => model.getValue()));
13681
13745
  const disposeDisposer = model.onWillDispose(() => {
13682
13746
  closeDoc(path);
13683
13747
  contentDisposer.dispose();
@@ -19045,10 +19109,38 @@ window.__ModuleLoader__.load({
19045
19109
  }, item.copyFrom ? item.copyFrom + "@" + (item.copyFromRev ?? "?") : ""), buttons);
19046
19110
  }
19047
19111
  /**
19112
+ * 单个数据行:浅比较包装(回调身份由父级 ref 转发保持稳定,故 memo 才可能命中)。
19113
+ * @author ddj 2026年10月14号
19114
+ */
19115
+ const LogItemRow = react.default.memo(LogItem);
19116
+ /**
19117
+ * 数据行容器(P0-14 上段)。独立 memo:只在条目列表/选中集/工作副本版号变化时重建行元素;
19118
+ * 上段高度拖拽与弹窗缩放只改 LogList 的 height,不再逐帧重建整表(原先每帧 logRowsOf + N 个元素)。
19119
+ * @author ddj 2026年10月14号
19120
+ * @param props.list 过滤排序后的条目(非空)
19121
+ * @param props.selectedRevs 可见选中的 revision 集合
19122
+ * @param props.wcRev 工作副本版号(null = 不加粗)
19123
+ * @param props.onPick 选中回调(event, item)
19124
+ * @param props.onMenu 右键回调(event, item)
19125
+ * @returns 行元素
19126
+ */
19127
+ const LogRows = react.default.memo(function LogRows(props) {
19128
+ const { list, selectedRevs, wcRev, onPick, onMenu } = props;
19129
+ return react.default.createElement(react.default.Fragment, null, logRowsOf(list).map((row) => react.default.createElement(LogItemRow, {
19130
+ key: row.item.revision + ":" + row.level,
19131
+ item: row.item,
19132
+ on: selectedRevs.has(row.item.revision),
19133
+ wc: wcRev != null && row.item.revision === wcRev,
19134
+ level: row.level,
19135
+ onPick,
19136
+ onMenu
19137
+ })));
19138
+ });
19139
+ /**
19048
19140
  * 上窗格列表(列头行 + 数据行 + 「加载更多」;P0-13 宽度受拖拽控制)。
19049
19141
  * @author ddj 2026年09月16号 / 2026年09月17号
19050
19142
  * @param props.list 过滤排序后的条目(null = 未加载)
19051
- * @param props.selected 选中条目数组(P1-3 多选;至少一条,由父级保证)
19143
+ * @param props.selectedRevs 可见选中的 revision 集合(P1-3 多选;由父级按 revision 维护)
19052
19144
  * @param props.keyword 过滤词(决定空态文案)
19053
19145
  * @param props.busy 加载中
19054
19146
  * @param props.truncated 是否可能还有更多
@@ -19061,19 +19153,17 @@ window.__ModuleLoader__.load({
19061
19153
  * @returns 列表元素
19062
19154
  */
19063
19155
  function LogList(props) {
19064
- const { list, selected, keyword, busy, truncated, sortKey, sortDir, onSort, onSortReset, onPick, onLoadMore, onItemMenu, height, wcRev } = props;
19156
+ const { list, selectedRevs, keyword, busy, truncated, sortKey, sortDir, onSort, onSortReset, onPick, onLoadMore, onItemMenu, height, wcRev } = props;
19065
19157
  let inner;
19066
19158
  if (list === null) inner = react.default.createElement("div", { className: "edrv-tree-loading" }, busy ? "读取日志中…" : "尚未加载");
19067
19159
  else if (!list.length) inner = react.default.createElement("div", { className: "edrv-tree-loading" }, keyword ? "无匹配条目" : "无提交历史");
19068
- else inner = logRowsOf(list).map((row) => react.default.createElement(LogItem, {
19069
- key: row.item.revision + ":" + row.level,
19070
- item: row.item,
19071
- on: selected.some((s) => s.revision === row.item.revision),
19072
- wc: wcRev != null && row.item.revision === wcRev,
19073
- level: row.level,
19160
+ else inner = react.default.createElement(LogRows, {
19161
+ list,
19162
+ selectedRevs,
19163
+ wcRev,
19074
19164
  onPick,
19075
19165
  onMenu: onItemMenu
19076
- }));
19166
+ });
19077
19167
  const more = truncated ? react.default.createElement("div", { className: "edrv-svnlog-more" }, react.default.createElement("button", {
19078
19168
  className: "edrv-svn-act",
19079
19169
  disabled: busy,
@@ -19393,7 +19483,9 @@ window.__ModuleLoader__.load({
19393
19483
  sortKey,
19394
19484
  sortDir
19395
19485
  ]);
19396
- const visibleSelected = sorted ? selected.filter((item) => sorted.some((x) => x.revision === item.revision)) : selected;
19486
+ const sortedRevs = react.default.useMemo(() => sorted ? new Set(sorted.map((x) => x.revision)) : null, [sorted]);
19487
+ const visibleSelected = react.default.useMemo(() => sortedRevs ? selected.filter((item) => sortedRevs.has(item.revision)) : selected, [sortedRevs, selected]);
19488
+ const selectedRevs = react.default.useMemo(() => new Set(visibleSelected.map((item) => item.revision)), [visibleSelected]);
19397
19489
  const current = visibleSelected.length ? visibleSelected[0] : sorted && sorted.length ? sorted[0] : null;
19398
19490
  /** 点列头:同键切换升降,异键回落升序;复位入口在列头行(G10)。 */
19399
19491
  const onSort = (key) => {
@@ -19492,24 +19584,26 @@ window.__ModuleLoader__.load({
19492
19584
  * @param wantList 目标上段(修订列表)高度
19493
19585
  * @param wantMsg 目标中段(提交信息)高度
19494
19586
  */
19587
+ /** @private @author ddj 2026年10月14号 @param nextList 目标上段高度 @param nextMsg 目标中段高度 只在值真正变化时提交。 */
19588
+ const applyFit = (nextList, nextMsg) => {
19589
+ if (nextList !== listH) setListH(nextList);
19590
+ if (nextMsg !== msgH) setMsgH(nextMsg);
19591
+ };
19495
19592
  const fitHeights = (wantList, wantMsg) => {
19496
19593
  const total = bodyRef.current?.clientHeight ?? 0;
19497
19594
  if (!total) {
19498
- setListH(wantList);
19499
- setMsgH(wantMsg);
19595
+ applyFit(wantList, wantMsg);
19500
19596
  return;
19501
19597
  }
19502
19598
  const paneOcc = paneRef.current?.offsetHeight ?? -1;
19503
19599
  const chrome = paneOcc >= 0 ? Math.max(0, total - listH - msgH - paneOcc) : HSPLIT_TOTAL_PX;
19504
19600
  const budget = total - PANE_KEEP_MIN - chrome;
19505
19601
  if (budget < 260) {
19506
- setListH(Math.min(wantList, LIST_H_MIN));
19507
- setMsgH(Math.min(wantMsg, MSG_H_MIN));
19602
+ applyFit(Math.min(wantList, LIST_H_MIN), Math.min(wantMsg, MSG_H_MIN));
19508
19603
  return;
19509
19604
  }
19510
19605
  if (wantList + wantMsg <= budget) {
19511
- setListH(wantList);
19512
- setMsgH(wantMsg);
19606
+ applyFit(wantList, wantMsg);
19513
19607
  return;
19514
19608
  }
19515
19609
  const scale = budget / (wantList + wantMsg);
@@ -19522,8 +19616,7 @@ window.__ModuleLoader__.load({
19522
19616
  nextMsg = MSG_H_MIN;
19523
19617
  nextList = Math.min(nextList, budget - nextMsg);
19524
19618
  }
19525
- setListH(nextList);
19526
- setMsgH(nextMsg);
19619
+ applyFit(nextList, nextMsg);
19527
19620
  };
19528
19621
  const fitRef = react.default.useRef(null);
19529
19622
  fitRef.current = () => fitHeights(listH, msgH);
@@ -19617,6 +19710,15 @@ window.__ModuleLoader__.load({
19617
19710
  ].filter(Boolean)
19618
19711
  });
19619
19712
  };
19713
+ /** @author ddj 2026年10月14号 */
19714
+ const rowCbs = react.default.useRef({
19715
+ pick: null,
19716
+ menu: null
19717
+ });
19718
+ rowCbs.current.pick = pickWithMods;
19719
+ rowCbs.current.menu = onItemMenu;
19720
+ const pickStable = react.default.useCallback((event, item) => rowCbs.current.pick?.(event, item), []);
19721
+ const menuStable = react.default.useCallback((event, item) => rowCbs.current.menu?.(event, item), []);
19620
19722
  /** 下窗格路径行右键菜单(P0-8;G8 目录不可开差异,G9 仓库外不可打开)。 */
19621
19723
  const onPathMenu = (event, item, revision) => {
19622
19724
  event.preventDefault();
@@ -19654,7 +19756,7 @@ window.__ModuleLoader__.load({
19654
19756
  ref: bodyRef
19655
19757
  }, react.default.createElement(LogList, {
19656
19758
  list: sorted,
19657
- selected: visibleSelected,
19759
+ selectedRevs,
19658
19760
  keyword: query.trim(),
19659
19761
  busy,
19660
19762
  truncated,
@@ -19662,9 +19764,9 @@ window.__ModuleLoader__.load({
19662
19764
  sortDir,
19663
19765
  onSort,
19664
19766
  onSortReset,
19665
- onPick: pickWithMods,
19767
+ onPick: pickStable,
19666
19768
  onLoadMore,
19667
- onItemMenu,
19769
+ onItemMenu: menuStable,
19668
19770
  height: listH,
19669
19771
  wcRev
19670
19772
  }), react.default.createElement(Splitter, {
@@ -26578,17 +26680,7 @@ window.__ModuleLoader__.load({
26578
26680
  "aria-label": "关闭提示",
26579
26681
  onClick: dismissHint
26580
26682
  }, "×")) : null;
26581
- const fullView = (0, react_dom.createPortal)(react.default.createElement("div", {
26582
- "data-edrv-view": "1",
26583
- style: {
26584
- display: "flex",
26585
- flexDirection: "column",
26586
- flex: 1,
26587
- minWidth: 0,
26588
- minHeight: 0,
26589
- overflow: "hidden"
26590
- }
26591
- }, pathBar, sideHintEl, diskBanner(), editorArea, statusBar), fullHost, "edrv-full-editor");
26683
+ const fullView = (0, react_dom.createPortal)(react.default.createElement(react.default.Fragment, null, pathBar, sideHintEl, diskBanner(), editorArea, statusBar), fullHost, "edrv-full-editor");
26592
26684
  const mainCol = react.default.createElement("div", {
26593
26685
  className: "edrv-main-col edrv-workbench",
26594
26686
  style: {
@@ -26609,13 +26701,7 @@ window.__ModuleLoader__.load({
26609
26701
  onBounds: floatBounds,
26610
26702
  onFocus: selectGroup,
26611
26703
  onReturn: returnPane
26612
- }, renderGroup(item.group))), dragPreview ? (0, react_dom.createPortal)(react.default.createElement("div", {
26613
- "data-edrv-view": "1",
26614
- style: {
26615
- display: "contents",
26616
- pointerEvents: "none"
26617
- }
26618
- }, dragPreview.rect ? react.default.createElement("div", {
26704
+ }, renderGroup(item.group))), dragPreview ? (0, react_dom.createPortal)(react.default.createElement(react.default.Fragment, null, dragPreview.rect ? react.default.createElement("div", {
26619
26705
  className: "edrv-drag-overlay",
26620
26706
  style: dragPreview.rect
26621
26707
  }, react.default.createElement("span", { className: "edrv-drop-label" }, dragPreview.label)) : null, react.default.createElement("div", {
@@ -29853,7 +29939,7 @@ window.__ModuleLoader__.load({
29853
29939
  const warnings = report?.warnings ?? [];
29854
29940
  const devForm = report?.devForm;
29855
29941
  const versionText = report?.dshVersion ? "(DSH " + report.dshVersion + " · 插件 " + (report?.pluginVersion ?? "") + ")" : report?.pluginVersion ? "(插件版本 " + report.pluginVersion + ")" : "";
29856
- return react.default.createElement("section", { className: "vsm-general-page" }, react.default.createElement("h2", null, "兼容性"), react.default.createElement("p", null, "检测与其他插件 / DSH 版本的适配状态与已知问题。" + versionText), error && react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, error), devForm?.enabled && group("开发形态", react.default.createElement("div", { className: "vsm-compat-item ok" }, react.default.createElement("span", { className: "vsm-compat-name" }, "开发形态开启(工作区链接安装)"), react.default.createElement("span", { className: "vsm-compat-note" }, devForm.path))), group("版本适配", renderItems(report?.adapters)), group("外部插件与服务", renderItems(report?.external)), group("客户端适配", renderItems(getSummary?.() ?? [])), group("护栏", renderItems(report?.guards)), group("警告", warnings.length ? warnings.map((w, index) => react.default.createElement("div", {
29942
+ return react.default.createElement("section", { className: "vsm-general-page" }, react.default.createElement("h2", null, "兼容性"), react.default.createElement("p", null, "检测与其他插件 / DSH 版本的适配状态与已知问题。" + versionText), error && react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, error), devForm?.enabled && group("开发形态", react.default.createElement("div", { className: "vsm-compat-item ok" }, react.default.createElement("span", { className: "vsm-compat-name" }, "开发形态开启(工作区链接安装)"), react.default.createElement("span", { className: "vsm-compat-note" }, devForm.path))), group("版本适配", renderItems(report?.adapters)), group("外部插件与服务", renderItems(report?.external)), group("客户端适配", renderItems(getSummary?.(report?.dshVersion ?? "") ?? [])), group("护栏", renderItems(report?.guards)), group("警告", warnings.length ? warnings.map((w, index) => react.default.createElement("div", {
29857
29943
  key: index,
29858
29944
  className: "vsm-mcp-error vsm-mcp-banner"
29859
29945
  }, w)) : react.default.createElement("div", { className: "vsm-compat-item ok" }, react.default.createElement("span", { className: "vsm-compat-name" }, "未检测到兼容性问题"), react.default.createElement("span", { className: "vsm-compat-note" }, ""))));
@@ -30741,6 +30827,12 @@ window.__ModuleLoader__.load({
30741
30827
  /** 深链移交轮询周期(兼活跃心跳)。 */
30742
30828
  const HANDOFF_POLL_MS = 3e3;
30743
30829
  /**
30830
+ * 页面隐藏时的降频倍数(3s × 5 = 15s)。
30831
+ * 必须仍短于 host 租约(src/externalHandoff.ts 的 LEASE_MS = 30s):心跳一旦超期,
30832
+ * host 判定该页面已死,launcher 会重复开新页——比多几次 RPC 更糟。
30833
+ */
30834
+ const HIDDEN_POLL_EVERY = 5;
30835
+ /**
30744
30836
  * 装配外部深链:深链参数缺失时仅保持移交轮询(零额外开销)。
30745
30837
  * @author ddj 2026年09月07号
30746
30838
  * @param ctx 客户端根上下文(sessions/workspaces 服务)
@@ -30763,9 +30855,10 @@ window.__ModuleLoader__.load({
30763
30855
  return dispose;
30764
30856
  }
30765
30857
  /**
30766
- * 深链移交轮询:每 3s 向 host 领取待打开请求(兼活跃心跳,launcher 据此决定是否开新页)。
30858
+ * 深链移交轮询:可见时每 3s 向 host 领取待打开请求(兼活跃心跳,launcher 据此决定是否开新页);
30859
+ * 页面隐藏时降到 15s 一次——无人看页面时移交不必保持 3s 精度,但心跳不能停(超期会让 host 判页面已死)。
30767
30860
  * 页面级单例(window 标记防 HMR 重复);领取到即执行打开规则。
30768
- * @author ddj 2026年09月08号
30861
+ * @author ddj 2026年09月08号 / 2026年10月15号
30769
30862
  * @param ctx 客户端根上下文
30770
30863
  */
30771
30864
  function startPresencePoll(ctx) {
@@ -30776,6 +30869,20 @@ window.__ModuleLoader__.load({
30776
30869
  let disposed = false;
30777
30870
  let polling = false;
30778
30871
  let executing = false;
30872
+ let ticks = 0;
30873
+ /**
30874
+ * 页面隐藏时按倍数跳轮;可见时每次都跑并清零计数。
30875
+ * @author ddj 2026年10月15号
30876
+ * @returns 是否跳过本轮
30877
+ */
30878
+ function hiddenSkip() {
30879
+ if (typeof document === "undefined" || document.visibilityState !== "hidden") {
30880
+ ticks = 0;
30881
+ return false;
30882
+ }
30883
+ ticks += 1;
30884
+ return ticks % HIDDEN_POLL_EVERY !== 0;
30885
+ }
30779
30886
  /** @private @author ddj 2026年09月28号 Open all requested paths before acknowledging this exact lease. */
30780
30887
  async function execute(open) {
30781
30888
  executing = true;
@@ -30807,6 +30914,7 @@ window.__ModuleLoader__.load({
30807
30914
  }
30808
30915
  }
30809
30916
  const timer = window.setInterval(() => {
30917
+ if (hiddenSkip()) return;
30810
30918
  tick().catch(() => {});
30811
30919
  }, HANDOFF_POLL_MS);
30812
30920
  /** @private @author ddj 2026年09月28号 Stop heartbeats and fail outstanding editor waits on plugin disposal. */
@@ -30882,6 +30990,80 @@ window.__ModuleLoader__.load({
30882
30990
  setTimeout(() => toast.remove(), TOAST_MS);
30883
30991
  }
30884
30992
  //#endregion
30993
+ //#region src/client/compatSummary.ts
30994
+ /**
30995
+ * Classify the API generation without importing the host's node-only version probe.
30996
+ * @private @author ddj 2026年10月13号
30997
+ * @param input Running DSH version from the compat RPC.
30998
+ * @param patch First supported 0.1.x patch (alpha.1).
30999
+ * @returns Whether that API generation is expected; undefined for unknown versions.
31000
+ */
31001
+ function apiExpected(input, patch) {
31002
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*))?(?:\+[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?$/.exec(input.trim());
31003
+ if (!match) return void 0;
31004
+ const major = Number(match[1]);
31005
+ const minor = Number(match[2]);
31006
+ const current = Number(match[3]);
31007
+ if (major !== 0) return major > 0;
31008
+ if (minor !== 1) return minor > 1;
31009
+ if (current !== patch) return current > patch;
31010
+ const pre = match[4];
31011
+ if (!pre) return true;
31012
+ const parts = pre.split(".");
31013
+ if (/^\d+$/.test(parts[0])) return false;
31014
+ if (parts[0] !== "alpha") return parts[0] > "alpha";
31015
+ return parts.length > 1 && (/^\d+$/.test(parts[1]) ? Number(parts[1]) >= 1 : true);
31016
+ }
31017
+ /**
31018
+ * Build only applicable diagnostics; explicit legacy choices still expose missing capabilities.
31019
+ * @public @author ddj 2026年10月13号
31020
+ * @param state Actual client installation facts and user selection.
31021
+ * @param version Running DSH version; unknown versions retain conservative diagnostics.
31022
+ * @returns Visible adapters, without changing routing or installation.
31023
+ */
31024
+ function clientSummary(state, version = "") {
31025
+ const remoteExpected = apiExpected(version, 3);
31026
+ const officialExpected = apiExpected(version, 5);
31027
+ const official = state.officialSidebar;
31028
+ const legacyChosen = state.selected === SIDEBAR_PLUGIN;
31029
+ const items = [{
31030
+ name: "设置桥",
31031
+ active: state.settingsReady,
31032
+ note: state.settingsReady ? "使用 " + state.settingsService + " 桥" : "未绑定设置服务(持久化不可用,晚到重试窗口内自愈)"
31033
+ }];
31034
+ if (remoteExpected !== true && !state.remoteReady && !official) items.push({
31035
+ name: "文件打开路由(workspaces.openPath)",
31036
+ active: state.openPathReady,
31037
+ note: "旧版对话文件链接路由,失败回退系统打开"
31038
+ });
31039
+ if (remoteExpected !== false || state.remoteReady) items.push({
31040
+ name: "会话文件链接路由(remote.session.openWorkspacePath)",
31041
+ active: state.remoteReady,
31042
+ note: state.remoteReady ? "会话文件链接走插件打开器,失败回退系统打开" : "适用路由未安装(服务缺失或补丁失败)"
31043
+ });
31044
+ if (legacyChosen || state.sidebarOpener || !official && officialExpected !== true) items.push({
31045
+ name: "侧边栏打开器(dsh-better-sidebar)",
31046
+ active: state.sidebarOpener,
31047
+ note: state.sidebarOpener ? "已注册(优先级 80)" : "未检测到侧边栏打开能力"
31048
+ });
31049
+ if (official || officialExpected !== false) items.push({
31050
+ name: "侧边栏编辑区(官方 Sidebar)",
31051
+ active: official,
31052
+ note: official ? "编辑区=官方右侧 Sidebar Tab(对话+编辑同屏)" : "官方侧边栏服务未探测到"
31053
+ });
31054
+ if (!official && (state.legacySidebar || officialExpected !== true)) items.push({
31055
+ name: "侧边栏编辑区(dsh-better-sidebar,归档)",
31056
+ active: state.legacySidebar,
31057
+ note: state.legacySidebar ? "编辑区=旧版侧边栏 Tab" : "旧版侧边栏回退能力未检测到"
31058
+ });
31059
+ if (state.fileClaim || state.claimWanted && (official || officialExpected !== false)) items.push({
31060
+ name: "文件链接官方认领(dsh-resource://file)",
31061
+ active: state.fileClaim,
31062
+ note: state.fileClaim ? "聊天文件链接转发进单一编辑器页签" : "官方文件链接认领未安装"
31063
+ });
31064
+ return items;
31065
+ }
31066
+ //#endregion
30885
31067
  //#region src/client/ui/SideEditorTab.ts
30886
31068
  /**
30887
31069
  * dsh-vscode-mode client — SideEditorTab:betterSidebar「文件编辑」Tab 的组件包装。
@@ -37841,44 +38023,24 @@ window.__ModuleLoader__.load({
37841
38023
  let claimDisposer = null;
37842
38024
  /** 「添加到对话」动作集:编辑区/Tab 右键菜单注入文件引用与代码块(conversation 服务缺失时各动作安全降级)。 */
37843
38025
  const addToConversation = createAddToConversation(ctx);
37844
- /** 客户端侧兼容摘要(与 host 报告合并展示;惰性求值保证 HMR 后仍新鲜)。 */
37845
- const compatSummary = () => [
37846
- {
37847
- name: "设置桥",
37848
- active: bridge.scope() !== void 0,
37849
- note: bridge.scope() !== void 0 ? "使用 " + bridge.service() + " 桥" : "未绑定设置服务(fileOpenTool 持久化不可用,晚到重试窗口内自愈)"
37850
- },
37851
- {
37852
- name: "文件打开路由(workspaces.openPath)",
37853
- active: Boolean(workspaces?.openPath),
37854
- note: "vscode 打开器优先,失败回退系统打开(0.1.2 及更早 DSH 的对话文件链接主路径)"
37855
- },
37856
- {
37857
- name: "会话文件链接路由(remote.session.openWorkspacePath)",
37858
- active: remoteOpenInstalled,
37859
- note: "0.1.3+ 对话文件引用/产物打开优先走插件打开器,失败回退系统打开"
37860
- },
37861
- {
37862
- name: "侧边栏打开器(dsh-better-sidebar)",
37863
- active: registry.get(SIDEBAR_PLUGIN) !== void 0,
37864
- note: registry.get("dsh-better-sidebar") !== void 0 ? "已注册(优先级 80)" : "未检测到侧边栏打开能力"
37865
- },
37866
- {
37867
- name: "侧边栏编辑区(官方 Sidebar)",
37868
- active: officialService !== void 0,
37869
- note: officialService !== void 0 ? "编辑区=官方右侧 Sidebar Tab(对话+编辑同屏,DSH 0.1.5+)" : "官方侧边栏服务未探测到(DSH < 0.1.5-alpha.1 时属预期)"
37870
- },
37871
- {
37872
- name: "侧边栏编辑区(dsh-better-sidebar,归档)",
37873
- active: sideService !== void 0,
37874
- note: sideService !== void 0 ? "编辑区=侧边栏 Tab(旧版 DSH 回退形态)" : "未检测到;DSH ≥ 0.1.5 优先官方侧边栏,旧版可安装(" + SIDEBAR_INSTALL_CMD + ")"
37875
- },
37876
- {
37877
- name: "文件链接官方认领(dsh-resource://file)",
37878
- active: claimDisposer !== null,
37879
- note: claimDisposer !== null ? "聊天文件链接转发进单一编辑器页签(文件分页归编辑器自带页签栏)" : "链接走官方查看器或旧版路由(未认领)"
37880
- }
37881
- ];
38026
+ /**
38027
+ * Build applicable client diagnostics from live installation state.
38028
+ * @private @author ddj 2026年10月13号
38029
+ * @param version Running DSH version supplied by the compat RPC.
38030
+ * @returns Applicable adapters; routing behavior is unchanged.
38031
+ */
38032
+ const compatSummary = (version = "") => clientSummary({
38033
+ settingsService: bridge.service(),
38034
+ settingsReady: bridge.scope() !== void 0,
38035
+ openPathReady: Boolean(workspaces?.openPath),
38036
+ remoteReady: remoteOpenInstalled,
38037
+ sidebarOpener: registry.get(SIDEBAR_PLUGIN) !== void 0,
38038
+ legacySidebar: sideService !== void 0,
38039
+ officialSidebar: officialService !== void 0,
38040
+ fileClaim: claimDisposer !== null,
38041
+ claimWanted: shouldClaimFiles(selected),
38042
+ selected
38043
+ }, version);
37882
38044
  ctx.provide("fileOpeners", registry);
37883
38045
  ctx.effect(() => registry.register(vscodeOpener()), "vscode-mode: file opener");
37884
38046
  const sidebarPanels = createSidebarPanelRegistry();