dsh-vscode-mode 0.26.0 → 0.27.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/lib/client.js CHANGED
@@ -414,6 +414,16 @@ window.__ModuleLoader__.load({
414
414
  }
415
415
  /** @private @author ddj 2026年10月14号 @param event Keyboard activation. */
416
416
  function key(event) {
417
+ if (event.key === "ContextMenu" || event.shiftKey && event.key === "F10") {
418
+ event.preventDefault();
419
+ event.stopPropagation();
420
+ const rect = event.currentTarget.getBoundingClientRect();
421
+ props.onMenu(tab.path, {
422
+ clientX: rect.left,
423
+ clientY: rect.bottom
424
+ });
425
+ return;
426
+ }
417
427
  if (event.key === "Enter" || event.key === " ") {
418
428
  event.preventDefault();
419
429
  props.onSelect(group.id, tab.path);
@@ -3469,11 +3479,23 @@ window.__ModuleLoader__.load({
3469
3479
  */
3470
3480
  function refreshFeed(sessionId, skipStale = false) {
3471
3481
  const entry = entryOf(sessionId);
3482
+ entry.queuedSkipStale = entry.queuedSkipStale === void 0 ? skipStale : entry.queuedSkipStale && skipStale;
3483
+ if (typeof document !== "undefined" && document.hidden) {
3484
+ entry.queued = true;
3485
+ return Promise.resolve();
3486
+ }
3487
+ if (entry.inflight) {
3488
+ entry.queued = true;
3489
+ return entry.request ?? Promise.resolve();
3490
+ }
3491
+ skipStale = entry.queuedSkipStale ?? skipStale;
3492
+ entry.queuedSkipStale = void 0;
3472
3493
  const current = ++entry.seq;
3473
3494
  entry.inflight = true;
3495
+ entry.controller = new AbortController();
3474
3496
  const payload = { sessionId };
3475
3497
  if (skipStale) payload.skipStale = true;
3476
- return rpc("edrv.list", payload).then((res) => {
3498
+ entry.request = rpc("edrv.list", payload, entry.controller.signal).then((res) => {
3477
3499
  if (current !== entry.seq) return;
3478
3500
  if (!res || !res.ok || !Array.isArray(res.records)) return;
3479
3501
  entry.records = res.records;
@@ -3485,8 +3507,14 @@ window.__ModuleLoader__.load({
3485
3507
  listener(message);
3486
3508
  } catch {}
3487
3509
  }).finally(() => {
3488
- if (current === entry.seq) entry.inflight = false;
3510
+ if (current !== entry.seq) return;
3511
+ entry.inflight = false;
3512
+ if (entry.queued) {
3513
+ entry.queued = false;
3514
+ if (feeds$1.get(sessionId) === entry) return refreshFeed(sessionId, entry.queuedSkipStale ?? false);
3515
+ }
3489
3516
  });
3517
+ return entry.request;
3490
3518
  }
3491
3519
  /**
3492
3520
  * 确保该会话的定时器与 refresh 事件监听已就绪(首个订阅者到来时启动)。
@@ -3496,12 +3524,26 @@ window.__ModuleLoader__.load({
3496
3524
  */
3497
3525
  function ensureTimer$1(sessionId, entry) {
3498
3526
  if (entry.timer === null) entry.timer = setInterval(() => {
3527
+ if (typeof document !== "undefined" && document.hidden) return;
3499
3528
  const tickers = Array.from(entry.tickers);
3500
3529
  for (const tick of tickers) try {
3501
3530
  tick();
3502
3531
  } catch {}
3503
3532
  if (entry.listeners.size > 0 || entry.errorListeners.size > 0) refreshFeed(sessionId);
3504
3533
  }, FEED_INTERVAL_MS);
3534
+ if (!entry.onVisible && typeof document !== "undefined") {
3535
+ entry.onVisible = () => {
3536
+ if (document.hidden) return;
3537
+ for (const tick of entry.tickers) try {
3538
+ tick();
3539
+ } catch {}
3540
+ if (entry.listeners.size || entry.errorListeners.size) {
3541
+ entry.queued = false;
3542
+ refreshFeed(sessionId);
3543
+ }
3544
+ };
3545
+ document.addEventListener("visibilitychange", entry.onVisible);
3546
+ }
3505
3547
  if (entry.onRefresh === null) {
3506
3548
  const handler = () => {
3507
3549
  refreshFeed(sessionId);
@@ -3518,12 +3560,16 @@ window.__ModuleLoader__.load({
3518
3560
  * @param entry 订阅槽位
3519
3561
  */
3520
3562
  function releaseIfIdle(sessionId, entry) {
3521
- if (entry.listeners.size || entry.tickers.size || entry.errorListeners.size) return;
3563
+ if (feeds$1.get(sessionId) !== entry || entry.listeners.size || entry.tickers.size || entry.errorListeners.size) return;
3522
3564
  if (entry.timer !== null) clearInterval(entry.timer);
3523
3565
  if (entry.onRefresh !== null) {
3524
3566
  const target = win();
3525
3567
  if (target) target.removeEventListener("edrv:refresh", entry.onRefresh);
3526
3568
  }
3569
+ entry.seq++;
3570
+ entry.queued = false;
3571
+ entry.controller?.abort();
3572
+ if (entry.onVisible && typeof document !== "undefined") document.removeEventListener("visibilitychange", entry.onVisible);
3527
3573
  feeds$1.delete(sessionId);
3528
3574
  }
3529
3575
  /**
@@ -3588,6 +3634,10 @@ window.__ModuleLoader__.load({
3588
3634
  /** @private @author ddj 2026年10月26号 @param sessionId Scope. @param entry Current generation. @returns First-page metadata update; no overlap or late publication. */
3589
3635
  async function refresh(sessionId, entry, force = false) {
3590
3636
  if (entry.controller.signal.aborted) return;
3637
+ if (typeof document !== "undefined" && document.hidden) {
3638
+ entry.again = true;
3639
+ return;
3640
+ }
3591
3641
  if (entry.loading) {
3592
3642
  entry.again = true;
3593
3643
  return;
@@ -3641,17 +3691,25 @@ window.__ModuleLoader__.load({
3641
3691
  };
3642
3692
  feeds.set(sessionId, entry);
3643
3693
  if (typeof window !== "undefined") window.addEventListener("edrv:refresh", onRefresh);
3694
+ if (typeof document !== "undefined") {
3695
+ const current = entry;
3696
+ entry.onVisible = () => {
3697
+ if (!document.hidden) refresh(sessionId, current);
3698
+ };
3699
+ document.addEventListener("visibilitychange", entry.onVisible);
3700
+ }
3644
3701
  refresh(sessionId, entry);
3645
3702
  }
3646
3703
  entry.listeners.add(listener);
3647
3704
  listener(entry.value);
3648
3705
  return () => {
3649
3706
  entry.listeners.delete(listener);
3650
- if (entry.listeners.size) return;
3707
+ if (entry.listeners.size || feeds.get(sessionId) !== entry) return;
3651
3708
  entry.controller.abort();
3652
3709
  clearTimeout(entry.timer);
3653
3710
  feeds.delete(sessionId);
3654
3711
  if (typeof window !== "undefined") window.removeEventListener("edrv:refresh", entry.onRefresh);
3712
+ if (entry.onVisible && typeof document !== "undefined") document.removeEventListener("visibilitychange", entry.onVisible);
3655
3713
  };
3656
3714
  }
3657
3715
  /** @public @author ddj 2026年10月26号 @param sessionId Refresh metadata after a decision; does not alter legacy or disk. */
@@ -376726,9 +376784,15 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
376726
376784
  */
376727
376785
  function resolveNavLine(params) {
376728
376786
  const line = (params ?? {})?.line;
376729
- if (typeof line !== "number" || !Number.isFinite(line) || line <= 0) return void 0;
376787
+ if (typeof line !== "number" || !Number.isFinite(line) || line < 1) return void 0;
376730
376788
  return Math.floor(line);
376731
376789
  }
376790
+ /** @public @author ddj 2026年11月04号 @param params Plugin navigation extension. @returns Positive column only alongside a valid line; official line-only callers remain unchanged. */
376791
+ function resolveNavColumn(params) {
376792
+ if (!resolveNavLine(params)) return void 0;
376793
+ const column = params?.column;
376794
+ return typeof column === "number" && Number.isFinite(column) && column >= 1 ? Math.floor(column) : void 0;
376795
+ }
376732
376796
  /** 逐段 component 解码(失败返回 null,对齐官方 parseFileAddress 的容错语义)。 */
376733
376797
  function decodeSegment(raw) {
376734
376798
  if (raw === void 0) return null;
@@ -376749,7 +376813,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
376749
376813
  */
376750
376814
  function parseOfficialFileAddress(address) {
376751
376815
  if (typeof address !== "string" || !address.startsWith("dsh-resource://file/")) return null;
376752
- const [scope, ...tail] = address.slice(20).split("/");
376816
+ const [scope, ...tail] = address.split(/[?#]/, 1)[0].slice(20).split("/");
376753
376817
  const kind = decodeSegment(scope);
376754
376818
  if (kind === "session") {
376755
376819
  const sessionId = decodeSegment(tail[0]);
@@ -376817,10 +376881,16 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
376817
376881
  * @param preview 可选 Markdown 渲染模式(行号导航优先源码)
376818
376882
  * @returns 转发是否成功(openTab 抛错返回 false,由调用方决定重试或兜底)
376819
376883
  */
376820
- function forwardToEditor(service, path, line, preview) {
376884
+ function forwardToEditor(service, path, line, preview, column) {
376821
376885
  if (!service || typeof service.openTab !== "function" || !path) return false;
376822
376886
  const params = { openPath: path };
376823
- if (line !== void 0) params.line = line;
376887
+ const validLine = resolveNavLine({ line });
376888
+ if (validLine !== void 0) params.line = validLine;
376889
+ const validColumn = resolveNavColumn({
376890
+ line: validLine,
376891
+ column
376892
+ });
376893
+ if (validColumn !== void 0) params.column = validColumn;
376824
376894
  if (preview !== void 0) params.preview = preview;
376825
376895
  try {
376826
376896
  service.openTab(OFFICIAL_TAB_KIND, { params });
@@ -386122,6 +386192,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
386122
386192
  at: 0
386123
386193
  };
386124
386194
  let progressListeners = /* @__PURE__ */ new Set();
386195
+ let statusRequest = null;
386196
+ let statusEpoch = 0;
386125
386197
  /** 会话 id 变更通知(EditorView 持有权威 sessionId,经窗口事件回填)。 */
386126
386198
  const SESSION_EVENT = "edrv:lsp-session";
386127
386199
  /** 上报可见状态(复用 EditorView 的状态通道;失败不再静默)。 */
@@ -386141,6 +386213,11 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
386141
386213
  syncTimers.clear();
386142
386214
  docVersions.clear();
386143
386215
  sessionId$1 = id;
386216
+ statusEpoch++;
386217
+ statusCache = {
386218
+ servers: [],
386219
+ at: 0
386220
+ };
386144
386221
  refreshStatus();
386145
386222
  }
386146
386223
  /**
@@ -386400,17 +386477,35 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
386400
386477
  if (!res || !res.tokens || !Array.isArray(res.tokens.data)) return null;
386401
386478
  return res.tokens;
386402
386479
  }
386403
- /** 拉取服务器状态(带 5s 缓存)。 */
386480
+ /** @public @author ddj 2026年11月04号 @param force Bypass cached data, not the in-flight guard. @returns Current-session server status; stale responses never publish. */
386404
386481
  async function refreshStatus(force = false) {
386482
+ if (statusRequest) {
386483
+ const pending = statusRequest;
386484
+ await pending.promise;
386485
+ if (pending.epoch !== statusEpoch) return refreshStatus(force);
386486
+ return statusCache.servers;
386487
+ }
386488
+ if (typeof document !== "undefined" && document.hidden) return statusCache.servers;
386405
386489
  if (!force && Date.now() - statusCache.at < 1500) return statusCache.servers;
386406
- const res = await rpc("edrv.lsp.status", { sessionId: sessionId$1 }).catch(() => null);
386407
- const servers = res && res.ok && Array.isArray(res.servers) ? res.servers : [];
386408
- statusCache = {
386409
- servers,
386410
- at: Date.now()
386490
+ const epoch = statusEpoch, owner = sessionId$1;
386491
+ const request = {
386492
+ epoch,
386493
+ promise: rpc("edrv.lsp.status", { sessionId: owner }).catch(() => null)
386411
386494
  };
386412
- publishProgress();
386413
- return servers;
386495
+ statusRequest = request;
386496
+ try {
386497
+ const res = await request.promise;
386498
+ if (epoch !== statusEpoch || owner !== sessionId$1) return statusCache.servers;
386499
+ const servers = res && res.ok && Array.isArray(res.servers) ? res.servers : [];
386500
+ statusCache = {
386501
+ servers,
386502
+ at: Date.now()
386503
+ };
386504
+ publishProgress();
386505
+ return servers;
386506
+ } finally {
386507
+ if (statusRequest === request) statusRequest = null;
386508
+ }
386414
386509
  }
386415
386510
  /** file:// 或已转 edrv:// 的 LSP location → Monaco Location(edrv:// uri + 1-based range)。 */
386416
386511
  function toClientLocation(loc) {
@@ -389483,6 +389578,13 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
389483
389578
  label: "添加到对话",
389484
389579
  disabled: !(hasSession && canAddToConversation)
389485
389580
  },
389581
+ ...state.forgeReady ? [{
389582
+ id: "forge-open",
389583
+ label: "打开 Forge"
389584
+ }, {
389585
+ id: "forge-local",
389586
+ label: "查看本地变更(Forge)"
389587
+ }] : [],
389486
389588
  {
389487
389589
  id: "close",
389488
389590
  label: "关闭",
@@ -394409,9 +394511,11 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
394409
394511
  page: null,
394410
394512
  error: ""
394411
394513
  });
394412
- if (!sessionId) return void 0;
394413
- return subscribeReview(sessionId, setReview);
394414
394514
  }, [sessionId]);
394515
+ react$1.default.useEffect(() => {
394516
+ if (!sessionId || props.visible === false) return void 0;
394517
+ return subscribeReview(sessionId, setReview);
394518
+ }, [sessionId, props.visible]);
394415
394519
  const [tabs, setTabs] = react$1.default.useState([]);
394416
394520
  const [active, setDiskActive] = react$1.default.useState(null);
394417
394521
  const [planTabs, setPlanTabs] = react$1.default.useState([]);
@@ -395720,7 +395824,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
395720
395824
  });
395721
395825
  };
395722
395826
  react$1.default.useEffect(() => {
395723
- if (!sessionId) return void 0;
395827
+ if (!sessionId || props.visible === false) return void 0;
395724
395828
  const offRecords = subscribeRecords(sessionId, applyRecords);
395725
395829
  const offErrors = subscribeErrors(sessionId, (message) => setError("list异常:" + message));
395726
395830
  refreshRecords();
@@ -395728,7 +395832,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
395728
395832
  offRecords();
395729
395833
  offErrors();
395730
395834
  };
395731
- }, [sessionId]);
395835
+ }, [sessionId, props.visible]);
395732
395836
  /**
395733
395837
  * 展示外部同步提示(按路径+类型去重:同一文件重复回调不重置已关闭的提示)。
395734
395838
  * @author ddj 2026年09月15号
@@ -395847,17 +395951,31 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
395847
395951
  onDiskChange: (change) => diskChangeRef.current?.(change)
395848
395952
  });
395849
395953
  react$1.default.useEffect(() => {
395850
- const publishLsp = (servers) => setLspServers(Array.isArray(servers) ? servers : []);
395954
+ let alive = true;
395955
+ let loading = false;
395956
+ const publishLsp = (servers) => {
395957
+ if (alive) setLspServers(Array.isArray(servers) ? servers : []);
395958
+ };
395959
+ const refreshLsp = async () => {
395960
+ if (loading || props.visible === false || document.hidden) return;
395961
+ loading = true;
395962
+ try {
395963
+ publishLsp(await refreshStatus(true));
395964
+ } finally {
395965
+ loading = false;
395966
+ }
395967
+ };
395851
395968
  const unsubscribe = onLspProgress(publishLsp);
395852
- refreshStatus(true).then(publishLsp);
395853
- const timer = setInterval(() => {
395854
- refreshStatus(true).then(publishLsp);
395855
- }, 1500);
395969
+ refreshLsp();
395970
+ const timer = setInterval(refreshLsp, 1500);
395971
+ document.addEventListener("visibilitychange", refreshLsp);
395856
395972
  return () => {
395973
+ alive = false;
395857
395974
  unsubscribe();
395858
395975
  clearInterval(timer);
395976
+ document.removeEventListener("visibilitychange", refreshLsp);
395859
395977
  };
395860
- }, [sessionId]);
395978
+ }, [sessionId, props.visible]);
395861
395979
  react$1.default.useEffect(() => {
395862
395980
  if (!sessionId) return;
395863
395981
  setSession(sessionId);
@@ -395907,6 +396025,9 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
395907
396025
  const fileOpenRef = react$1.default.useRef(null);
395908
396026
  /** @private @author ddj 2026年10月08号 @param e Routed file request. Always uses the current workspace and save/preview callbacks. */
395909
396027
  fileOpenRef.current = (e) => {
396028
+ if (e?.detail?.sessionId && e.detail.sessionId !== sessionId) return;
396029
+ if (e?.detail?.ownerId && e.detail.ownerId !== props?.ownerId) return;
396030
+ if (!e?.detail?.ownerId && props?.visible === false) return;
395910
396031
  const p = e?.detail?.path;
395911
396032
  if (!p) return;
395912
396033
  const normalized = tabPath(p);
@@ -398670,6 +398791,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
398670
398791
  }
398671
398792
  const tabMenuActions = (path) => ({
398672
398793
  "add-to-conversation": () => addRefToChat(path),
398794
+ "forge-open": () => props.forge?.open(sessionId),
398795
+ "forge-local": () => props.forge?.open(sessionId, true),
398673
398796
  "split-left": () => splitTab(path, "left"),
398674
398797
  "split-right": () => splitTab(path, "right"),
398675
398798
  "split-top": () => splitTab(path, "top"),
@@ -398716,6 +398839,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
398716
398839
  cwd,
398717
398840
  hasSession: Boolean(sessionId),
398718
398841
  canAddToConversation: Boolean(addToConversation),
398842
+ forgeReady: props.forge?.available() === true,
398719
398843
  canSplit: workbench.groups.length < 8 && !workbench.floats.some((item) => item.group === ownerOf(workbench, tabMenu.path)?.id),
398720
398844
  canFloat: workbench.groups.length < 8,
398721
398845
  canReturn: ownerOf(workbench, tabMenu.path)?.id !== MAIN_GROUP,
@@ -399098,6 +399222,48 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
399098
399222
  y: event.clientY
399099
399223
  });
399100
399224
  }
399225
+ const menuOwner = react$1.default.useRef({
399226
+ sessionId,
399227
+ scope,
399228
+ alive: true
399229
+ });
399230
+ menuOwner.current.sessionId = sessionId;
399231
+ menuOwner.current.scope = scope;
399232
+ const menuValid = react$1.default.useRef(null);
399233
+ react$1.default.useEffect(() => {
399234
+ menuOwner.current.alive = props.visible !== false;
399235
+ return () => {
399236
+ menuOwner.current.alive = false;
399237
+ if (menuValid.current) props.officialMenus?.invalidate(menuValid.current);
399238
+ };
399239
+ }, [
399240
+ sessionId,
399241
+ scope,
399242
+ props.visible,
399243
+ props.officialMenus
399244
+ ]);
399245
+ react$1.default.useEffect(() => {
399246
+ if (!tabMenu || !props.officialMenus || !sessionId) return;
399247
+ const capturedSession = sessionId;
399248
+ const capturedScope = scope;
399249
+ const path = tabMenu.path;
399250
+ const groupId = ownerOf(benchRef.current, path)?.id;
399251
+ const valid = () => menuOwner.current.alive && menuOwner.current.sessionId === capturedSession && menuOwner.current.scope === capturedScope && ownerOf(benchRef.current, path)?.id === groupId && tabsRef.current.some((tab) => tab.path === path);
399252
+ menuValid.current = valid;
399253
+ const items = tabMenuEntries().map((entry) => ({
399254
+ ...entry,
399255
+ run: entry.onClick ?? (() => {})
399256
+ }));
399257
+ if (props.officialMenus.open("tab", sessionId, path, {
399258
+ x: tabMenu.x,
399259
+ y: tabMenu.y
399260
+ }, items, valid)) setTabMenu(null);
399261
+ }, [
399262
+ tabMenu,
399263
+ props.officialMenus,
399264
+ sessionId,
399265
+ scope
399266
+ ]);
399101
399267
  /** @private @author ddj 2026年10月14号 @param group Auxiliary group. Return documents rather than closing them. */
399102
399268
  function returnPane(group) {
399103
399269
  acceptLayout(returnGroup(benchRef.current, group));
@@ -399877,6 +400043,9 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
399877
400043
  editor: () => editorRef.current,
399878
400044
  outlineSources: props.outlineSources,
399879
400045
  fileMenuItems: props.fileMenuItems,
400046
+ officialMenus: props.officialMenus,
400047
+ visible: props.visible,
400048
+ forge: props.forge,
399880
400049
  addToConversation,
399881
400050
  svn: svnStatus,
399882
400051
  svnChanges,
@@ -403933,6 +404102,144 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
403933
404102
  return new McpSessionState(fetchStatus, document);
403934
404103
  }
403935
404104
  //#endregion
404105
+ //#region src/client/forgeBridge.ts
404106
+ /** @public @author ddj 2026年11月04号 @param ctx Client capabilities. @param current Current session reader. @returns Capability-gated official navigation, with no polling or writes. */
404107
+ function forgeBridge(ctx, current) {
404108
+ const capabilities = () => {
404109
+ try {
404110
+ return {
404111
+ tabs: ctx.get("sidebarRightTabs"),
404112
+ service: ctx.get("sidebarRight")
404113
+ };
404114
+ } catch {
404115
+ return;
404116
+ }
404117
+ };
404118
+ return {
404119
+ available() {
404120
+ const capabilitiesNow = capabilities();
404121
+ return Boolean(capabilitiesNow?.tabs?.get?.("forge-panel") && capabilitiesNow.tabs.get("forge-changes") && capabilitiesNow.service?.openTab && capabilitiesNow.service.openResource);
404122
+ },
404123
+ open(sessionId, local = false) {
404124
+ if (!sessionId || current() !== sessionId || !this.available()) return false;
404125
+ const service = capabilities().service;
404126
+ try {
404127
+ if (local) service.openResource("dsh-resource://forge-changes/" + encodeURIComponent(sessionId) + "/local", { kind: "forge-changes" });
404128
+ else service.openTab("forge-panel");
404129
+ return true;
404130
+ } catch {
404131
+ return false;
404132
+ }
404133
+ }
404134
+ };
404135
+ }
404136
+ //#endregion
404137
+ //#region src/client/contextMenusOfficial.ts
404138
+ let ownerSeq = 0;
404139
+ /** @public @author ddj 2026年11月04号 @param ctx Optional service context. @returns Retry-on-use bridge for late service registration, with legacy fallback. */
404140
+ function lazyMenus(ctx) {
404141
+ let bridge;
404142
+ let stopped = false;
404143
+ return {
404144
+ open(...args) {
404145
+ if (stopped) return false;
404146
+ bridge ??= officialMenus(ctx);
404147
+ return bridge?.open(...args) ?? false;
404148
+ },
404149
+ invalidate(valid) {
404150
+ bridge?.invalidate(valid);
404151
+ },
404152
+ dispose() {
404153
+ stopped = true;
404154
+ bridge?.dispose();
404155
+ bridge = void 0;
404156
+ }
404157
+ };
404158
+ }
404159
+ /** @public @author ddj 2026年11月04号 @param ctx Optional service context. @returns Scoped bridge or no capability, preserving the existing popup. */
404160
+ function officialMenus(ctx) {
404161
+ let service;
404162
+ try {
404163
+ service = ctx.get("contextMenus");
404164
+ } catch {
404165
+ return;
404166
+ }
404167
+ if (!service || typeof service.register !== "function" || typeof service.open !== "function" || typeof service.close !== "function") return void 0;
404168
+ const owner = "edrv-menu-" + ++ownerSeq;
404169
+ let current;
404170
+ let revision = 0;
404171
+ let stopped = false;
404172
+ const disposers = [];
404173
+ try {
404174
+ for (const kind of ["resource", "tab"]) disposers.push(service.register({
404175
+ id: owner + "-" + kind,
404176
+ target: "edrv." + kind,
404177
+ order: 30,
404178
+ resolve: (target, signal) => {
404179
+ const opening = current;
404180
+ if (stopped || target.owner !== owner || !opening || !opening.valid() || signal.aborted) return [];
404181
+ return opening.items.map((item) => ({
404182
+ id: item.id,
404183
+ label: item.label,
404184
+ disabled: item.disabled,
404185
+ danger: item.danger,
404186
+ separatorBefore: item.separator,
404187
+ commandId: item.commandId,
404188
+ run: async () => {
404189
+ if (stopped || opening.revision !== revision || !opening.valid() || item.disabled) return;
404190
+ await item.run();
404191
+ }
404192
+ }));
404193
+ }
404194
+ }));
404195
+ } catch {
404196
+ for (const dispose of disposers) dispose();
404197
+ return;
404198
+ }
404199
+ /** @private @author ddj 2026年11月04号 Close only this bridge's active shared popup. */
404200
+ function closeOwned() {
404201
+ const active = service.active?.getSnapshot();
404202
+ if (active?.value?.owner === owner && (active.kind === "edrv.resource" || active.kind === "edrv.tab")) service.close();
404203
+ }
404204
+ return {
404205
+ open(kind, sessionId, path, point, items, valid) {
404206
+ if (stopped || !sessionId || !valid()) return false;
404207
+ current = {
404208
+ items,
404209
+ valid,
404210
+ revision: ++revision
404211
+ };
404212
+ try {
404213
+ service.open({
404214
+ kind: "edrv." + kind,
404215
+ value: {
404216
+ owner,
404217
+ sessionId,
404218
+ path
404219
+ }
404220
+ }, point);
404221
+ return true;
404222
+ } catch {
404223
+ current = void 0;
404224
+ return false;
404225
+ }
404226
+ },
404227
+ invalidate(valid) {
404228
+ if (current?.valid !== valid) return;
404229
+ revision++;
404230
+ current = void 0;
404231
+ closeOwned();
404232
+ },
404233
+ dispose() {
404234
+ stopped = true;
404235
+ revision++;
404236
+ current = void 0;
404237
+ closeOwned();
404238
+ for (const dispose of disposers) dispose();
404239
+ }
404240
+ };
404241
+ }
404242
+ //#endregion
403936
404243
  //#region src/client/ui/McpSessionStatus.ts
403937
404244
  /** Official session-row slots; no DOM patching or session activation. @author ddj 2026年10月20号 */
403938
404245
  const labels = {
@@ -404271,6 +404578,16 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
404271
404578
  active: state.fileClaim,
404272
404579
  note: state.fileClaim ? "聊天文件链接转发进单一编辑器页签" : "官方文件链接认领未安装"
404273
404580
  });
404581
+ if (state.officialMenus !== void 0) items.push({
404582
+ name: "官方右键菜单",
404583
+ active: state.officialMenus,
404584
+ note: state.officialMenus ? "插件动作接入共享菜单" : "服务缺失或装配失败,保留插件菜单"
404585
+ });
404586
+ if (state.forge !== void 0) items.push({
404587
+ name: "Forge 导航",
404588
+ active: state.forge,
404589
+ note: state.forge ? "导航至官方面板与本地变更;PR 保持官方审查" : "官方 Forge 未安装或未启用,不自动启用"
404590
+ });
404274
404591
  return items;
404275
404592
  }
404276
404593
  //#endregion
@@ -404339,8 +404656,12 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
404339
404656
  const sessionId = props?.sessionId;
404340
404657
  const useTabInfo = props?.useTabInfo;
404341
404658
  let navigation;
404659
+ let tab;
404660
+ let visible = true;
404342
404661
  if (typeof useTabInfo === "function") try {
404343
- navigation = useTabInfo()?.tab?.navigation;
404662
+ tab = useTabInfo()?.tab;
404663
+ navigation = tab?.navigation;
404664
+ visible = tab?.visible !== false;
404344
404665
  } catch {
404345
404666
  navigation = void 0;
404346
404667
  }
@@ -404371,19 +404692,25 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
404371
404692
  if (revision <= 0) return;
404372
404693
  const fileHit = parseOfficialFileAddress(navigation?.address);
404373
404694
  if (fileHit) {
404374
- const line = navigation?.params?.line;
404695
+ const line = resolveNavLine(navigation?.params);
404375
404696
  window.dispatchEvent(new CustomEvent("edrv:open-editor", { detail: {
404697
+ sessionId,
404698
+ ownerId: tab?.id,
404376
404699
  path: fileHit.path,
404377
- line: typeof line === "number" ? line : void 0,
404700
+ line,
404701
+ column: resolveNavColumn(navigation?.params),
404378
404702
  preview: navigation?.params?.preview
404379
404703
  } }));
404380
404704
  return;
404381
404705
  }
404382
404706
  const request = resolveNavOpen(navigation?.params);
404383
404707
  window.dispatchEvent(new CustomEvent("edrv:open-editor", { detail: {
404708
+ sessionId,
404709
+ ownerId: tab?.id,
404384
404710
  path: request.path,
404385
404711
  focusDiff: request.focusDiff,
404386
404712
  line: resolveNavLine(navigation?.params),
404713
+ column: resolveNavColumn(navigation?.params),
404387
404714
  preview: navigation?.params?.preview
404388
404715
  } }));
404389
404716
  });
@@ -404391,11 +404718,15 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
404391
404718
  key: sessionId,
404392
404719
  sessionId,
404393
404720
  layout: "side",
404721
+ visible,
404722
+ ownerId: tab?.id,
404394
404723
  schedule: props?.schedule,
404395
404724
  addToConversation: props?.addToConversation,
404396
404725
  sidebarPanels: props?.sidebarPanels,
404397
404726
  outlineSources: props?.outlineSources,
404398
404727
  fileMenuItems: props?.fileMenuItems,
404728
+ officialMenus: props?.officialMenus,
404729
+ forge: props?.forge,
404399
404730
  fileOpeners: props?.fileOpeners,
404400
404731
  sessions: props?.sessions
404401
404732
  });
@@ -404462,17 +404793,27 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
404462
404793
  const [failed, setFailed] = react$1.default.useState(false);
404463
404794
  const doneRef = react$1.default.useRef(-1);
404464
404795
  react$1.default.useEffect(() => {
404465
- if (revision === doneRef.current) return;
404466
- doneRef.current = revision;
404796
+ const intent = JSON.stringify([
404797
+ props?.sessionId,
404798
+ tab?.id,
404799
+ revision,
404800
+ navigation?.address
404801
+ ]);
404802
+ if (intent === doneRef.current) return;
404467
404803
  const parsed = parseOfficialFileAddress(navigation?.address);
404468
404804
  if (!parsed) {
404469
404805
  setFailed(true);
404470
404806
  return;
404471
404807
  }
404472
404808
  const line = resolveNavLine(navigation?.params);
404809
+ const column = resolveNavColumn(navigation?.params);
404810
+ let alive = true;
404811
+ setFailed(false);
404473
404812
  let left = FORWARD_ATTEMPTS;
404474
404813
  const attempt = () => {
404475
- if (forwardToEditor(service, parsed.path, line, navigation?.params?.preview)) {
404814
+ if (!alive || tab?.signal?.aborted) return;
404815
+ if (forwardToEditor(typeof tab?.actions?.openTab === "function" ? tab.actions : service, parsed.path, line, navigation?.params?.preview, column)) {
404816
+ doneRef.current = intent;
404476
404817
  closeTab(tab);
404477
404818
  return;
404478
404819
  }
@@ -404481,7 +404822,15 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
404481
404822
  else setFailed(true);
404482
404823
  };
404483
404824
  attempt();
404484
- }, [revision]);
404825
+ return () => {
404826
+ alive = false;
404827
+ };
404828
+ }, [
404829
+ revision,
404830
+ props?.sessionId,
404831
+ tab?.id,
404832
+ navigation?.address
404833
+ ]);
404485
404834
  if (!failed || typeof fallback !== "function") return null;
404486
404835
  return react$1.default.createElement(fallback, props);
404487
404836
  }
@@ -408716,13 +409065,26 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
408716
409065
  function menu(event) {
408717
409066
  onContext(path, type, event);
408718
409067
  }
409068
+ /** @private @author ddj 2026年11月04号 @param event Keyboard context trigger anchored to the focused row. */
409069
+ function menuKey(event) {
409070
+ if (event.key !== "ContextMenu" && !(event.shiftKey && event.key === "F10")) return;
409071
+ const rect = event.currentTarget.getBoundingClientRect();
409072
+ onContext(path, type, {
409073
+ preventDefault: () => event.preventDefault(),
409074
+ stopPropagation: () => event.stopPropagation(),
409075
+ clientX: rect.left,
409076
+ clientY: rect.bottom
409077
+ });
409078
+ }
408719
409079
  return react$1.default.createElement("div", {
408720
409080
  className: "edrv-tree-row" + (active ? " edrv-tree-active" : "") + (context ? " edrv-tree-context" : "") + (reveal ? " edrv-tree-reveal" : "") + (mark?.stale ? " edrv-tree-stale" : ""),
408721
409081
  title: path + (mark?.stale ? ";SVN 状态陈旧,等待确认" : ""),
408722
409082
  "data-edrv-path": path,
408723
409083
  style: { paddingLeft: 6 + depth * 14 },
409084
+ tabIndex: 0,
408724
409085
  onClick: select,
408725
- onContextMenu: menu
409086
+ onContextMenu: menu,
409087
+ onKeyDown: menuKey
408726
409088
  }, react$1.default.createElement("span", { className: "edrv-tree-chev" }, isDir ? isOpen ? "▾" : "▸" : ""), react$1.default.createElement("span", { className: "edrv-tree-iconbox" }, rowIconEl({ name }, isDir, isOpen), svnOverlayEl(viewOverlayOf(mark), remote)), react$1.default.createElement("span", { className: "edrv-tree-name" + (type === "other" ? " edrv-tree-dim" : "") }, name), pending > 0 ? react$1.default.createElement("span", { className: "edrv-tree-badge" }, String(pending)) : null);
408727
409089
  }
408728
409090
  const ExplorerRow = react$1.default.memo(rowView);
@@ -409182,6 +409544,54 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
409182
409544
  if (menu && !menuValid) setMenu(null);
409183
409545
  }
409184
409546
  react$1.default.useEffect(closeLostMenu, [menu, menuValid]);
409547
+ const liveMenu = react$1.default.useRef({
409548
+ scope,
409549
+ treeRows,
409550
+ sessionId: ctx?.sessionId,
409551
+ alive: true
409552
+ });
409553
+ const openingValid = react$1.default.useRef(null);
409554
+ liveMenu.current = {
409555
+ scope,
409556
+ treeRows,
409557
+ sessionId: ctx?.sessionId,
409558
+ alive: liveMenu.current.alive
409559
+ };
409560
+ react$1.default.useEffect(() => {
409561
+ liveMenu.current.alive = ctx?.visible !== false;
409562
+ return () => {
409563
+ liveMenu.current.alive = false;
409564
+ if (openingValid.current) ctx?.officialMenus?.invalidate(openingValid.current);
409565
+ };
409566
+ }, [
409567
+ scope,
409568
+ ctx?.sessionId,
409569
+ ctx?.officialMenus,
409570
+ ctx?.visible
409571
+ ]);
409572
+ react$1.default.useEffect(() => {
409573
+ if (!menuValid || !ctx?.officialMenus || !ctx.sessionId) return;
409574
+ const target = { ...menu.target };
409575
+ const sessionId = ctx.sessionId;
409576
+ const valid = () => liveMenu.current.alive && liveMenu.current.sessionId === sessionId && liveMenu.current.scope === menu.scope && (target.path === "" || liveMenu.current.treeRows.some((row) => row.e?.path === target.path));
409577
+ openingValid.current = valid;
409578
+ const items = buildTreeMenu(ctx.fileMenuItems, target, ctx).map((item) => ({
409579
+ id: item.id,
409580
+ label: item.label,
409581
+ disabled: item.disabled,
409582
+ danger: item.danger,
409583
+ separator: item.separator,
409584
+ run: () => item.run(target, ctx)
409585
+ }));
409586
+ if (ctx.officialMenus.open("resource", ctx.sessionId, target.path, {
409587
+ x: menu.x,
409588
+ y: menu.y
409589
+ }, items, valid)) setMenu(null);
409590
+ }, [
409591
+ menu,
409592
+ menuValid,
409593
+ ctx?.officialMenus
409594
+ ]);
409185
409595
  const menuEntries = menuValid ? buildTreeMenu(ctx?.fileMenuItems, menu.target, ctx).map((item) => ({
409186
409596
  id: item.id,
409187
409597
  label: item.label,
@@ -412672,6 +413082,26 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
412672
413082
  function createDefaultFileMenuItems() {
412673
413083
  return [
412674
413084
  ...openGroupItems(),
413085
+ {
413086
+ id: "forge-open",
413087
+ label: "打开 Forge",
413088
+ order: 6,
413089
+ group: GROUP_OPEN,
413090
+ visible: (_target, ctx) => ctx.forge?.available() === true,
413091
+ run: (_target, ctx) => {
413092
+ if (ctx.sessionId) ctx.forge?.open(ctx.sessionId);
413093
+ }
413094
+ },
413095
+ {
413096
+ id: "forge-local",
413097
+ label: "查看本地变更(Forge)",
413098
+ order: 7,
413099
+ group: GROUP_OPEN,
413100
+ visible: (_target, ctx) => ctx.forge?.available() === true,
413101
+ run: (_target, ctx) => {
413102
+ if (ctx.sessionId) ctx.forge?.open(ctx.sessionId, true);
413103
+ }
413104
+ },
412675
413105
  ...revealGroupItems(),
412676
413106
  ...refGroupItems(),
412677
413107
  ...clipGroupItems(),
@@ -414513,7 +414943,9 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
414513
414943
  officialSidebar: officialService !== void 0,
414514
414944
  fileClaim: claimDisposer !== null,
414515
414945
  claimWanted: shouldClaimFiles(selected),
414516
- selected
414946
+ selected,
414947
+ officialMenus: Boolean(ctx.get("contextMenus")),
414948
+ forge: forge.available()
414517
414949
  }, version);
414518
414950
  ctx.provide("fileOpeners", registry);
414519
414951
  ctx.effect(() => registry.register(vscodeOpener()), "vscode-mode: file opener");
@@ -414545,12 +414977,17 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
414545
414977
  const sidebar = scanSidebar(ctx);
414546
414978
  return sidebar ? registry.register(sidebar) : void 0;
414547
414979
  }, "vscode-mode: sidebar file opener");
414980
+ const forge = forgeBridge(ctx, () => readSessionScope(ctx).sessionId ?? void 0);
414981
+ const menuBridge = lazyMenus(ctx);
414982
+ if (menuBridge) ctx.effect(() => () => menuBridge.dispose(), "vscode-mode: official menus");
414548
414983
  const officialRenderTab = (props) => react$1.default.createElement(OfficialSideTab, Object.assign({}, props, {
414549
414984
  schedule,
414550
414985
  addToConversation,
414551
414986
  sidebarPanels,
414552
414987
  outlineSources,
414553
414988
  fileMenuItems,
414989
+ officialMenus: menuBridge,
414990
+ forge,
414554
414991
  fileOpeners: registry,
414555
414992
  sessions,
414556
414993
  readSessionScope: () => readSessionScope(ctx)
@@ -414735,6 +415172,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
414735
415172
  sidebarPanels,
414736
415173
  outlineSources,
414737
415174
  fileMenuItems,
415175
+ officialMenus: menuBridge,
415176
+ forge,
414738
415177
  fileOpeners: registry,
414739
415178
  sessions
414740
415179
  })));
@@ -414820,6 +415259,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
414820
415259
  sidebarPanels,
414821
415260
  outlineSources,
414822
415261
  fileMenuItems,
415262
+ officialMenus: menuBridge,
415263
+ forge,
414823
415264
  fileOpeners: registry,
414824
415265
  sessions
414825
415266
  })),
@@ -414929,6 +415370,17 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
414929
415370
  compatSummary,
414930
415371
  getShortcuts: () => detectShortcuts(ctx)
414931
415372
  })));
415373
+ registerSlotSafely(ctx, {
415374
+ name: "settings.section.icon",
415375
+ id: "vscode-mode",
415376
+ order: 30
415377
+ }, (input) => {
415378
+ const props = input;
415379
+ return react$1.default.createElement(IconCodeOutline16, {
415380
+ size: props.size ?? 16,
415381
+ className: props.active ? "edrv-settings-active" : void 0
415382
+ });
415383
+ });
414932
415384
  registerSlotSafely(ctx, {
414933
415385
  name: "plugins.detail.section",
414934
415386
  id: "edrv-plugin-status",