dsh-vscode-mode 0.25.1 → 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.
Files changed (42) hide show
  1. package/README.md +25 -11
  2. package/lib/client.js +1969 -1344
  3. package/lib/client.js.map +2 -2
  4. package/lib/index.js +1443 -716
  5. package/lib/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/client/compatSummary.ts +4 -0
  8. package/src/client/contextMenusOfficial.ts +80 -0
  9. package/src/client/forgeBridge.ts +28 -0
  10. package/src/client/index.ts +18 -3
  11. package/src/client/monaco/lsp/lspClient.ts +25 -7
  12. package/src/client/officialSidebar.ts +15 -3
  13. package/src/client/reviewBar.ts +3 -0
  14. package/src/client/reviewBatch.ts +9 -1
  15. package/src/client/reviewKeepAll.ts +72 -0
  16. package/src/client/sidebar/menuItems.ts +6 -0
  17. package/src/client/sidebar/panels/FileExplorer.ts +26 -1
  18. package/src/client/sidebar/types.ts +3 -0
  19. package/src/client/state/diffSummary.ts +34 -7
  20. package/src/client/state/recordsFeed.ts +40 -3
  21. package/src/client/state/reviewFeed.ts +9 -2
  22. package/src/client/tabMenu.ts +5 -0
  23. package/src/client/ui/ClaimRouter.ts +11 -5
  24. package/src/client/ui/EditorPane.ts +9 -1
  25. package/src/client/ui/EditorView.ts +164 -39
  26. package/src/client/ui/OfficialSideTab.ts +13 -5
  27. package/src/client/ui/ReviewAllConfirm.ts +1 -1
  28. package/src/index.ts +1 -1
  29. package/src/pending/capture.ts +96 -47
  30. package/src/pending/persist.ts +94 -5
  31. package/src/pending/store.ts +171 -58
  32. package/src/pendingCompat.ts +72 -9
  33. package/src/pendingRpc.ts +10 -10
  34. package/src/reviewCapture.ts +64 -23
  35. package/src/reviewDb.ts +119 -8
  36. package/src/reviewEvidence.ts +1 -1
  37. package/src/reviewRpc.ts +115 -9
  38. package/src/rpc.ts +14 -10
  39. package/src/shared/review.ts +21 -0
  40. package/src/shared/rpc.ts +3 -1
  41. package/src/store.ts +44 -0
  42. package/src/toolTargets.ts +63 -0
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. */
@@ -3686,7 +3744,15 @@ window.__ModuleLoader__.load({
3686
3744
  if (page.nextCursor !== null || page.items.length !== page.pending) throw new Error("待审记录数量不完整,未执行批量操作");
3687
3745
  return page.items;
3688
3746
  }
3689
- /** @public @author ddj 2026年10月27号 @param port Guarded existing per-file decision adapter. @param reject Undo instead of Keep. @returns Precise partial result; Undo stops at first conflict, never claims atomic multi-file rollback. */
3747
+ /**
3748
+ * @public
3749
+ * @author ddj 2026年10月27号
3750
+ * @description: 逐条决策引擎,现仅供 Undo All 使用(Keep 已改走 `reviewKeepAll.ts` 的宿主批量通道);
3751
+ * 保留 reject 形态以便对照既有契约与测试。
3752
+ * @param port Guarded existing per-file decision adapter.
3753
+ * @param reject Undo instead of Keep.
3754
+ * @returns Precise partial result; Undo stops at first conflict, never claims atomic multi-file rollback.
3755
+ */
3690
3756
  async function runReviewBatch(port, reject) {
3691
3757
  const result = {
3692
3758
  completed: 0,
@@ -3721,6 +3787,1278 @@ window.__ModuleLoader__.load({
3721
3787
  return result;
3722
3788
  }
3723
3789
  //#endregion
3790
+ //#region src/client/reviewKeepAll.ts
3791
+ /**
3792
+ * @private
3793
+ * @author ddj 2026年11月03号
3794
+ * @description: 取一次完整待审快照;完整性校验失败时重取一次,避免瞬时漂移被当成不可恢复错误。
3795
+ * @param port 已鉴权的 RPC 适配器
3796
+ * @returns 冻结的批量目标
3797
+ * @throws 两次都取不到一致快照时抛出原始原因
3798
+ */
3799
+ async function snapshotOnce(port) {
3800
+ try {
3801
+ return toItems(await loadPending(port));
3802
+ } catch {
3803
+ port.guard();
3804
+ return toItems(await loadPending(port));
3805
+ }
3806
+ }
3807
+ /**
3808
+ * @private
3809
+ * @author ddj 2026年11月03号
3810
+ * @description: 把快照条目裁成批量目标,只带 host 需要的身份与版本线索。
3811
+ * @param items 完整待审集
3812
+ * @returns 批量目标
3813
+ */
3814
+ function toItems(items) {
3815
+ return items.map((item) => ({
3816
+ id: item.id,
3817
+ display: item.display,
3818
+ seq: item.seq,
3819
+ revision: item.revision
3820
+ }));
3821
+ }
3822
+ /**
3823
+ * @public
3824
+ * @author ddj 2026年11月03号
3825
+ * @description: 批量保留全部待审记录(Keep 不改写磁盘)。一次往返完成,跳过项只进 skipped,
3826
+ * 段级异常才写 error;guard 失效时保留已完成计数交给调用方呈现。
3827
+ * @param port 已鉴权的 RPC 适配器
3828
+ * @returns 与 BatchResult 同构的批量结果(含跳过明细)
3829
+ */
3830
+ async function runKeepAll(port) {
3831
+ const result = {
3832
+ completed: 0,
3833
+ total: 0,
3834
+ paths: [],
3835
+ skipped: [],
3836
+ error: ""
3837
+ };
3838
+ try {
3839
+ port.guard();
3840
+ const items = await snapshotOnce(port);
3841
+ result.total = items.length;
3842
+ if (!items.length) return result;
3843
+ port.guard();
3844
+ const reply = await port.keepAll(items, crypto.randomUUID());
3845
+ if (!reply.ok) {
3846
+ result.error = reply.error;
3847
+ return result;
3848
+ }
3849
+ return reply.result;
3850
+ } catch (cause) {
3851
+ result.error = cause instanceof Error ? cause.message : String(cause);
3852
+ return result;
3853
+ }
3854
+ }
3855
+ //#endregion
3856
+ //#region src/shared/externalOpen.ts
3857
+ /**
3858
+ * dsh-vscode-mode shared — 外部深链 URL 契约(Windows launcher / Unity 包 / client 共用)。
3859
+ * 形态:?<FLAG>=1&<PATHS_KEY>=<enc1>[,<enc2>…] [&<LINE_KEY>=N] [&<COLUMN_KEY>=M]
3860
+ * 每个路径独立 percent-encode(encodeURIComponent / Uri.EscapeDataString)后以 `,` 连接:
3861
+ * 编码结果不含裸逗号(Windows 路径本身不允许逗号,编码兜底其余字符),URLSearchParams
3862
+ * 取值做一次标准解码即得 `enc1,enc2`,按 `,` split 即路径,无需二次 decode。
3863
+ * Purity rule: no node/react imports(URLSearchParams 为标准全局)。
3864
+ * 作者 ddj 2026-09-07
3865
+ */
3866
+ /** 深链开关参数名(值恒为 '1')。 */
3867
+ const EDRV_OPEN_FLAG = "edrvOpen";
3868
+ /** 路径列表参数名(逗号分隔的已编码路径)。 */
3869
+ const EDRV_PATHS_KEY = "edrvPaths";
3870
+ /** 目标行参数名(1-based,可选,作用于首个路径)。 */
3871
+ const EDRV_LINE_KEY = "edrvLine";
3872
+ /** 目标列参数名(1-based,可选,作用于首个路径)。 */
3873
+ const EDRV_COLUMN_KEY = "edrvColumn";
3874
+ /** 深链参数键全集(URL 清理用)。 */
3875
+ const EDRV_PARAM_KEYS = [
3876
+ EDRV_OPEN_FLAG,
3877
+ EDRV_PATHS_KEY,
3878
+ EDRV_LINE_KEY,
3879
+ EDRV_COLUMN_KEY
3880
+ ];
3881
+ /**
3882
+ * 解析深链查询串:非深链/路径为空 → null;行列仅接受正整数,非法忽略。
3883
+ * @author ddj 2026年09月07号
3884
+ * @param search location.search(含或不含前导 ? 均可)
3885
+ * @returns 深链参数或 null
3886
+ */
3887
+ function parseOpenParams(search) {
3888
+ const params = new URLSearchParams(search ?? "");
3889
+ if (params.get("edrvOpen") !== "1") return null;
3890
+ const paths = (params.get("edrvPaths") ?? "").split(",").map((item) => item.trim()).filter(Boolean);
3891
+ if (!paths.length) return null;
3892
+ return {
3893
+ paths,
3894
+ line: positiveIntOf(params.get(EDRV_LINE_KEY)),
3895
+ column: positiveIntOf(params.get(EDRV_COLUMN_KEY))
3896
+ };
3897
+ }
3898
+ /**
3899
+ * 正整数解析(非法/非正 → undefined)。
3900
+ * @author ddj 2026年09月07号
3901
+ * @param raw 参数原文(可空)
3902
+ * @returns 正整数或 undefined
3903
+ */
3904
+ function positiveIntOf(raw) {
3905
+ const value = Number(raw);
3906
+ return Number.isInteger(value) && value > 0 ? value : void 0;
3907
+ }
3908
+ //#endregion
3909
+ //#region src/client/chooseDialog.ts
3910
+ /**
3911
+ * dsh-vscode-mode client — 工作区选择弹窗(深链打开规则 1)。
3912
+ * 复用与「添加 MCP」一致的 vsm-mcp-modal / vsm-mcp-dialog 模态(页面居中卡片 + 遮罩 + 主题
3913
+ * token),React portal 挂 body;Esc / 遮罩点击 / 取消 → null(中止流程不改任何状态)。
3914
+ * 作者 ddj 2026-09-07
3915
+ */
3916
+ /** 选择弹窗(vsm-mcp-modal 遮罩 + vsm-mcp-dialog 卡片,Esc/遮罩点击取消)。 */
3917
+ function ChoiceModal({ title, folder, done }) {
3918
+ react$1.default.useEffect(() => {
3919
+ const onKey = (event) => {
3920
+ if (event.key === "Escape") done(null);
3921
+ };
3922
+ window.addEventListener("keydown", onKey);
3923
+ return () => window.removeEventListener("keydown", onKey);
3924
+ }, []);
3925
+ return react$1.default.createElement("div", {
3926
+ className: "vsm-mcp-modal",
3927
+ onMouseDown: (event) => {
3928
+ if (event.target === event.currentTarget) done(null);
3929
+ }
3930
+ }, react$1.default.createElement("div", { className: "vsm-mcp-dialog" }, react$1.default.createElement("h3", null, "在 DSH 中打开"), react$1.default.createElement("label", null, "文件夹 " + folder + " 位于已注册的工作区「" + title + "」内,选择打开方式:"), react$1.default.createElement("div", { className: "vsm-mcp-dialog-actions" }, react$1.default.createElement("button", { onClick: () => done(null) }, "取消"), react$1.default.createElement("button", { onClick: () => done("create") }, "新建工作区"), react$1.default.createElement("button", {
3931
+ className: "vsm-primary",
3932
+ onClick: () => done("recent")
3933
+ }, "使用最近的工作区"))));
3934
+ }
3935
+ /**
3936
+ * 弹出工作区选择弹窗(深链打开规则 1)。
3937
+ * @author ddj 2026年09月07号
3938
+ * @param title 匹配到的工作区标题
3939
+ * @param folder 文件夹路径
3940
+ * @returns 用户选择;null = 取消/中止
3941
+ */
3942
+ function chooseWorkspace(title, folder) {
3943
+ return new Promise((resolve) => {
3944
+ const host = document.createElement("div");
3945
+ document.body.appendChild(host);
3946
+ const root = (0, react_dom_client.createRoot)(host);
3947
+ const done = (choice) => {
3948
+ root.unmount();
3949
+ host.remove();
3950
+ resolve(choice);
3951
+ };
3952
+ root.render(react$1.default.createElement(ChoiceModal, {
3953
+ title,
3954
+ folder,
3955
+ done
3956
+ }));
3957
+ });
3958
+ }
3959
+ //#endregion
3960
+ //#region src/client/addToConversation.ts
3961
+ /**
3962
+ * dsh-vscode-mode client — 把文件/选中内容注入会话对话输入框(composer draft)。
3963
+ * 复用 DSH 官方输入门面:ctx.conversation.input.for(sessions.scope(sessionId)) → SessionInput。
3964
+ * 引用 chip:insertReference(reference source,发送时序列化为 @path 文本);忙态降级为纯文本 setDraft。
3965
+ * 纯函数部分 node 可单测。
3966
+ * 作者 ddj 2026-08-25
3967
+ */
3968
+ /** 把追加引用结果映射为可读文案(ok 用 okText;busy 提示已降级纯文本;unavailable 提示不可用)。
3969
+ * @author ddj 2026年09月03号 / 2026年09月11号
3970
+ * @param outcome 追加结果状态
3971
+ * @param okText 成功文案(如「已添加文件引用」/「已添加文件夹引用」)
3972
+ * @returns 状态栏/通知用文案
3973
+ */
3974
+ function statusOfAdd(outcome, okText) {
3975
+ if (outcome === "ok") return okText;
3976
+ if (outcome === "busy") return okText + "(已降级纯文本)";
3977
+ if (outcome === "failed") return "添加引用失败(输入框忙或未就绪,请重试)";
3978
+ return "无法添加到对话(无会话或输入框不可用)";
3979
+ }
3980
+ /** DSH reference source 名(dsh-client-ui-reference 注册的 @file/@session 统一源)。 */
3981
+ const REF_SOURCE = "reference";
3982
+ /**
3983
+ * 把 clipboard 投影的末尾折算成 **detect 投影**的末尾。
3984
+ *
3985
+ * ⚠️ 两个投影对 chip 的计长不同,混用会直接毁掉已插入的引用(真实缺陷,勿改回 `draft.length`):
3986
+ * DSH 的 `$composerLayout()` 中 chip 在 detect 投影里只贡献 **1 个 U+FFFC**,
3987
+ * 在 clipboard 投影里贡献 `clipboardText` 全文
3988
+ * (`pushLeaf('chip', kid, '', kid.getTextContent())`)。
3989
+ * 而 `insertReference(ref, span)` 的 span 走 detect 坐标并经 `selectSpan` 校验:
3990
+ * `span.end > layout.detectLength` 时返回 null → 插入失败。
3991
+ *
3992
+ * 旧实现直接用 `draft.length`(clipboard 长度)当 detect 起点:草稿里一旦有 chip,
3993
+ * 该值就超出 detect 长度 → 插入被拒 → 落入 `setDraft` 兜底,
3994
+ * 而 `setDraft` 会整篇重建并剔除 U+FFFC(`text.replace(REFERENCE_PLACEHOLDER_RE, '')`),
3995
+ * 于是**既有 chip 被销毁、引用退化为重复的纯文本**(即「连续添加多个引用显示异常」)。
3996
+ *
3997
+ * 换算:每个 chip 在 clipboard 中多算了 `length - 1`,逐个扣回即得 detect 末尾。
3998
+ * 无 occurrences(旧版 DSH)时退化为 `draft.length`,与旧行为一致。
3999
+ * @author ddj 2026年09月11号
4000
+ * @param snapshot 输入快照
4001
+ * @returns detect 投影中的文档末尾偏移
4002
+ */
4003
+ function detectEndOf(snapshot) {
4004
+ const total = String(snapshot?.draft ?? "").length;
4005
+ const occurrences = snapshot?.occurrences;
4006
+ if (!Array.isArray(occurrences) || !occurrences.length) return total;
4007
+ let clipped = 0;
4008
+ for (const occurrence of occurrences) {
4009
+ const length = Number(occurrence?.length ?? 0);
4010
+ if (!Number.isFinite(length) || length <= 1) continue;
4011
+ clipped += length - 1;
4012
+ }
4013
+ return Math.max(0, total - clipped);
4014
+ }
4015
+ /** chip 列表按 clipboard 偏移升序(官方已排序,此处防御性归一,丢弃非法项)。 */
4016
+ function chipsOf(snapshot) {
4017
+ const occurrences = snapshot?.occurrences;
4018
+ if (!Array.isArray(occurrences) || !occurrences.length) return [];
4019
+ const out = [];
4020
+ for (const occurrence of occurrences) {
4021
+ const offset = Number(occurrence?.offset ?? NaN);
4022
+ const length = Number(occurrence?.length ?? NaN);
4023
+ if (!Number.isFinite(offset) || !Number.isFinite(length) || length <= 1) continue;
4024
+ out.push({
4025
+ offset: Math.max(0, offset),
4026
+ length
4027
+ });
4028
+ }
4029
+ return out.sort((a, b) => a.offset - b.offset);
4030
+ }
4031
+ /**
4032
+ * detect 偏移 → clipboard 偏移(paddingAt 用:draft 是 clipboard 投影,不能直接按下标取字符)。
4033
+ * 每个位于该偏移之前的 chip 在 clipboard 中多占 `length − 1`,累计补上即可。
4034
+ * @author ddj 2026年09月11号
4035
+ * @param snapshot 输入快照
4036
+ * @param detectOffset detect 投影偏移
4037
+ * @returns clipboard 投影偏移(已夹到 draft 范围内)
4038
+ */
4039
+ function detectToClipboardOf(snapshot, detectOffset) {
4040
+ const total = String(snapshot?.draft ?? "").length;
4041
+ const target = Math.max(0, Math.min(Number(detectOffset) || 0, detectEndOf(snapshot)));
4042
+ let shift = 0;
4043
+ for (const chip of chipsOf(snapshot)) {
4044
+ if (chip.offset - shift >= target) break;
4045
+ shift += chip.length - 1;
4046
+ }
4047
+ return Math.max(0, Math.min(target + shift, total));
4048
+ }
4049
+ /**
4050
+ * 插入点两侧是否需要补空格(仅纯文本通道需要;chip 由 facade 自行补尾随空格)。
4051
+ *
4052
+ * 在 detect 坐标的任意位置求值,故**末尾追加的行为与旧实现逐字符一致**
4053
+ * (末尾前为非空白 → 前后各补一个空格;末尾前已是空白 → 只补尾随空格),
4054
+ * 同时正确支持「插在文本中间」不再贴字。
4055
+ * @author ddj 2026年09月11号
4056
+ * @param snapshot 输入快照
4057
+ * @param detectOffset detect 投影插入点
4058
+ * @returns 是否需要前导/尾随空格
4059
+ */
4060
+ function paddingAt(snapshot, detectOffset) {
4061
+ const draft = String(snapshot?.draft ?? "");
4062
+ const at = detectToClipboardOf(snapshot, detectOffset);
4063
+ const before = at > 0 ? draft.slice(at - 1, at) : "";
4064
+ const after = at < draft.length ? draft.slice(at, at + 1) : "";
4065
+ return {
4066
+ lead: before !== "" && !/\s/.test(before),
4067
+ tail: after === "" || !/\s/.test(after)
4068
+ };
4069
+ }
4070
+ /**
4071
+ * 解析插入点(detect 坐标):优先用输入框**当前光标/选区**(`caretSpan()`,
4072
+ * 官方契约即 detect 坐标,无选区时回落文档末尾),并夹到 `[0, detectLength]`。
4073
+ *
4074
+ * 选区非塌缩时取 `end`(右边界)—— 插入而非替换,绝不删除用户已选中的内容。
4075
+ * caret 缺失/非法(旧版 facade 无该方法、抛异常、NaN 等)一律回落到文档末尾。
4076
+ * @author ddj 2026年09月11号
4077
+ * @param caret caretSpan() 的返回值(可为 null/非法)
4078
+ * @param detectLength detect 投影文档长度
4079
+ * @returns detect 投影插入偏移
4080
+ */
4081
+ function insertOffsetOf(caret, detectLength) {
4082
+ const max = Math.max(0, Number(detectLength) || 0);
4083
+ if (!caret || typeof caret !== "object") return max;
4084
+ const span = caret;
4085
+ const raw = Number(span.end);
4086
+ const value = Number.isFinite(raw) ? raw : Number(span.start);
4087
+ if (!Number.isFinite(value)) return max;
4088
+ return Math.max(0, Math.min(value, max));
4089
+ }
4090
+ /**
4091
+ * 生成 DSH @file 语法引用串:cwd 相对化、\ → /、含空白时按 @"path" 语法加引号。
4092
+ * cwd 外/无法相对化的路径回退原路径。
4093
+ * @author ddj 2026年08月25号
4094
+ * @param path 打开的文件路径(相对 cwd 或绝对)
4095
+ * @param cwd 会话工作区目录(可选)
4096
+ * @returns 引用串(如 @src/index.ts 或 @"a b.ts")
4097
+ */
4098
+ function mentionOf(path, cwd) {
4099
+ const raw = String(path);
4100
+ let rel = raw;
4101
+ if (cwd) {
4102
+ const base = String(cwd).replace(/[\\/]+$/, "");
4103
+ const up = raw.replace(/\\/g, "/");
4104
+ const baseUp = base.replace(/\\/g, "/");
4105
+ if (up.startsWith(baseUp + "/")) rel = up.slice(baseUp.length + 1);
4106
+ else if (up === baseUp) rel = "";
4107
+ }
4108
+ rel = rel.replace(/\\/g, "/").replace(/^\.\//, "");
4109
+ if (!rel) rel = raw.replace(/\\/g, "/");
4110
+ if (/[\u0000-\u001f\u007f-\u009f"]/u.test(rel)) return "@" + raw;
4111
+ return /\s/u.test(rel) ? `@"${rel}"` : `@${rel}`;
4112
+ }
4113
+ /** 引用标签:文件名 + 可选行区间(chip 内展示,不带 @ 前缀)。
4114
+ * @author ddj 2026年08月25号 */
4115
+ function labelOf(path, range) {
4116
+ const base = String(path).split(/[\\/]/).pop() || String(path);
4117
+ if (!range || range.endLine < range.startLine) return base;
4118
+ return range.startLine === range.endLine ? `${base} L${range.startLine}` : `${base} L${range.startLine}-${range.endLine}`;
4119
+ }
4120
+ /** 引用 ref:引用串 + 可选行区间(序列化直出,agent 据此读取对应片段)。
4121
+ * @author ddj 2026年08月25号 */
4122
+ function refOf(mention, range) {
4123
+ if (!range || range.endLine < range.startLine) return mention;
4124
+ return range.startLine === range.endLine ? `${mention} L${range.startLine}` : `${mention} L${range.startLine}-${range.endLine}`;
4125
+ }
4126
+ /** 构造 DSH 文件引用插入载荷,模型文本、剪贴板投影与降级文本共用完整行区间。
4127
+ * @public
4128
+ * @author ddj 2026年10月28号
4129
+ * @returns 引用载荷与含可选行区间的完整引用文本
4130
+ * @param path 目标路径(相对 cwd 或绝对)
4131
+ * @param cwd 会话工作区目录(可选)
4132
+ * @param range 行区间(可选)
4133
+ * @param appearance 引用外观:'file'(默认)| 'folder'
4134
+ */
4135
+ function buildFileRef(path, cwd, range, appearance = "file") {
4136
+ const mention = refOf(mentionOf(path, cwd), range);
4137
+ return {
4138
+ mention,
4139
+ reference: {
4140
+ source: REF_SOURCE,
4141
+ ref: mention,
4142
+ label: labelOf(path, range),
4143
+ appearance,
4144
+ clipboardText: mention
4145
+ }
4146
+ };
4147
+ }
4148
+ /** @private @author ddj 2026年10月29号 @param receipt Raw selected-channel receipt. @returns Explicit admission evidence and the historical boolean. */
4149
+ function taskReceipt(receipt) {
4150
+ const ok = receipt?.ok;
4151
+ if (ok === true) return {
4152
+ status: "accepted",
4153
+ legacyOk: true
4154
+ };
4155
+ if (ok === false) return {
4156
+ status: "rejected",
4157
+ legacyOk: false
4158
+ };
4159
+ return {
4160
+ status: "unknown",
4161
+ legacyOk: false
4162
+ };
4163
+ }
4164
+ /** @private @author ddj 2026年10月29号 @param ctx Lifecycle owner. @param sessions Public Controller. @param id Fixed target. @param text Task body. @returns Admission evidence without switching transport. @sideEffects Retains and releases exactly one generation. */
4165
+ async function sendOwned(ctx, sessions, id, text) {
4166
+ const controller = new AbortController();
4167
+ const reference = sessions.retain(id, {
4168
+ source: "controllerOperation",
4169
+ signal: controller.signal
4170
+ });
4171
+ let released = false;
4172
+ let cleanupFailed = false;
4173
+ /** @private @author ddj 2026年10月29号 Cancels and releases this exact generation once; remembers unload cleanup failures. */
4174
+ function release() {
4175
+ if (released) return;
4176
+ released = true;
4177
+ controller.abort();
4178
+ try {
4179
+ reference.release();
4180
+ } catch (error) {
4181
+ cleanupFailed = true;
4182
+ throw error;
4183
+ }
4184
+ }
4185
+ let remove;
4186
+ let started = false;
4187
+ let result = {
4188
+ status: "unavailable",
4189
+ legacyOk: false
4190
+ };
4191
+ try {
4192
+ remove = ctx.effect?.(() => release, "vscode-mode: task session reference");
4193
+ const binding = await reference.ready;
4194
+ if (controller.signal.aborted) result = {
4195
+ status: "cancelled",
4196
+ legacyOk: false
4197
+ };
4198
+ else {
4199
+ started = true;
4200
+ result = taskReceipt(await binding.session.prompt([{
4201
+ type: "text",
4202
+ text
4203
+ }], "queue", controller.signal));
4204
+ }
4205
+ } catch {
4206
+ result = {
4207
+ status: started ? "unknown" : "unavailable",
4208
+ legacyOk: false
4209
+ };
4210
+ }
4211
+ const cancelled = controller.signal.aborted;
4212
+ try {
4213
+ release();
4214
+ } catch {
4215
+ cleanupFailed = true;
4216
+ }
4217
+ try {
4218
+ remove?.();
4219
+ } catch {
4220
+ cleanupFailed = true;
4221
+ }
4222
+ if (cleanupFailed) result = {
4223
+ status: started ? "unknown" : "unavailable",
4224
+ legacyOk: false
4225
+ };
4226
+ if (cancelled) result = {
4227
+ status: "cancelled",
4228
+ legacyOk: result.legacyOk
4229
+ };
4230
+ return result;
4231
+ }
4232
+ /** @private @author ddj 2026年10月29号 @param ctx Service owner. @param id Fixed target. @param text Task body. @returns One legacy-channel attempt; no cross-channel retry. */
4233
+ async function sendLegacy(ctx, id, text) {
4234
+ const input = inputFor(ctx, id);
4235
+ if (typeof input?.setDraft === "function" && typeof input.submit === "function") {
4236
+ let submitted = false;
4237
+ try {
4238
+ input.setDraft(text);
4239
+ submitted = true;
4240
+ input.submit();
4241
+ return {
4242
+ status: "submitted",
4243
+ legacyOk: true
4244
+ };
4245
+ } catch {
4246
+ return {
4247
+ status: submitted ? "unknown" : "unavailable",
4248
+ legacyOk: false
4249
+ };
4250
+ }
4251
+ }
4252
+ const remote = ctx.get("remote.session");
4253
+ if (typeof remote?.prompt !== "function") return httpTaskResult(id, text);
4254
+ try {
4255
+ const payload = {
4256
+ sessionId: id,
4257
+ mode: "queue",
4258
+ content: [{
4259
+ type: "text",
4260
+ text
4261
+ }]
4262
+ };
4263
+ const receipt = await remote.prompt(slotArg(payload));
4264
+ return taskReceipt(receipt?.result ?? receipt);
4265
+ } catch {
4266
+ return {
4267
+ status: "unknown",
4268
+ legacyOk: false
4269
+ };
4270
+ }
4271
+ }
4272
+ /** apiproxy wire 端点(POST /api/<method>;浏览器同源 fetch 自带 cookie 鉴权;实证 401=未鉴权而非 404)。 */
4273
+ const SESSION_CREATE_PATH = "/api/session/create";
4274
+ /** apiproxy wire 端点:发送任务(session/prompt;点击即发送方案)。 */
4275
+ const SESSION_PROMPT_PATH = "/api/session/prompt";
4276
+ /** mint RPC 请求 id(RpcId 品牌类型运行时即普通字符串)。 */
4277
+ function mintRpcId() {
4278
+ try {
4279
+ return globalThis.crypto?.randomUUID?.() ?? "rpc-" + Date.now();
4280
+ } catch {
4281
+ return "rpc-" + Date.now();
4282
+ }
4283
+ }
4284
+ /**
4285
+ * wire 槽实参组装(gateway `invoke` 铁证:`request.args[parameter.wire]` 具名槽,
4286
+ * `session/prompt` 的槽名 = "request",值 = RpcRequest `{rpcId, payload}`)。
4287
+ * @author ddj 2026年09月24号
4288
+ * @param payload 业务载荷
4289
+ * @returns 具名槽实参 `{ args: { request: RpcRequest } }`
4290
+ */
4291
+ function slotArg(payload) {
4292
+ return { args: { request: {
4293
+ rpcId: mintRpcId(),
4294
+ payload
4295
+ } } };
4296
+ }
4297
+ /**
4298
+ * RPC 返回解析(照官方 dsh-client-runtime 调用形态:`const { result } = await api.sessions.create(...)`)。
4299
+ * 官方 api.sessions 面返回**信封** `{ rpcId, result: RpcResult }`——须解 .result;
4300
+ * 兼容裸 RpcResult 形态(个别补丁面直接回 {ok, value})。
4301
+ * @author ddj 2026年09月23号
4302
+ * @param res 信封或裸 RpcResult
4303
+ * @returns 归一后的 { ok, value }
4304
+ */
4305
+ function rpcOk(res) {
4306
+ const r = res?.result ?? res;
4307
+ return {
4308
+ ok: r?.ok === true,
4309
+ value: r?.value
4310
+ };
4311
+ }
4312
+ /**
4313
+ * wire 直调 session/create(主路径):ClientRequest 信封 → ServerResponse.result(RpcResult 形态)。
4314
+ * 契约以当前安装版 dsh-host-apiproxy 为准(api/sessions.d.ts「Method signatures are the
4315
+ * source of truth」):`create({cwd}) → {sessionId}`;信封 = `{type:'client-request', rpcId,
4316
+ * method, payload}` → `{result: {ok:true, value:{sessionId}} | {ok:false, error}}`。
4317
+ * @author ddj 2026年09月23号
4318
+ * @param cwd 新会话工作区目录
4319
+ * @returns 新会话 id(失败空串)
4320
+ */
4321
+ async function apiSessionCreate(cwd) {
4322
+ const json = await (await fetch(appUrl(SESSION_CREATE_PATH), {
4323
+ method: "POST",
4324
+ headers: { "content-type": "application/json" },
4325
+ body: JSON.stringify({
4326
+ type: "client-request",
4327
+ rpcId: mintRpcId(),
4328
+ method: "session/create",
4329
+ payload: cwd ? { cwd } : {}
4330
+ })
4331
+ })).json().catch(() => null);
4332
+ const id = json?.result?.value?.sessionId;
4333
+ return json?.result?.ok === true && typeof id === "string" ? id : "";
4334
+ }
4335
+ /** @private @author ddj 2026年10月29号 @param sessionId Fixed target. @param text Task body. @returns HTTP receipt evidence; errors after request entry are never safe rejection. */
4336
+ async function httpTaskResult(sessionId, text) {
4337
+ try {
4338
+ const payload = {
4339
+ sessionId,
4340
+ mode: "queue",
4341
+ content: [{
4342
+ type: "text",
4343
+ text
4344
+ }]
4345
+ };
4346
+ return taskReceipt((await (await fetch(appUrl(SESSION_PROMPT_PATH), {
4347
+ method: "POST",
4348
+ headers: { "content-type": "application/json" },
4349
+ body: JSON.stringify({
4350
+ type: "client-request",
4351
+ rpcId: mintRpcId(),
4352
+ method: "session/prompt",
4353
+ payload: slotArg(payload)
4354
+ })
4355
+ })).json().catch(() => null))?.result);
4356
+ } catch {
4357
+ return {
4358
+ status: "unknown",
4359
+ legacyOk: false
4360
+ };
4361
+ }
4362
+ }
4363
+ /**
4364
+ * 新建独立会话承载旁路任务(02-deep-session-prompt:深度分析不污染当前对话)。
4365
+ * 优先公开 sessions.create;旧 Host 按能力选择 remote 或 wire,调用后不换通道重试。
4366
+ * 受理状态不明时返回 ok:false,不重复创建会话。
4367
+ * @author ddj 2026年10月28号
4368
+ * @author ddj 2026年09月23号
4369
+ * @param ctx 客户端服务上下文(remote.session + sessions,仅回落用)
4370
+ * @param cwd 新会话工作区目录(与当前会话同工作区)
4371
+ * @returns 新会话结果(含 id)
4372
+ */
4373
+ async function newDraftSession(ctx, cwd) {
4374
+ try {
4375
+ const sessions = ctx.get("sessions");
4376
+ if (typeof sessions?.create === "function") {
4377
+ const created = await sessions.create(cwd ? { cwd } : void 0);
4378
+ const id = typeof created === "string" && created ? created : typeof created?.id === "string" ? created.id : typeof created?.sessionId === "string" ? created.sessionId : typeof created?.session?.id === "string" ? created.session.id : "";
4379
+ if (id) return {
4380
+ ok: true,
4381
+ id
4382
+ };
4383
+ }
4384
+ if (typeof sessions?.create === "function") return { ok: false };
4385
+ } catch {
4386
+ return { ok: false };
4387
+ }
4388
+ try {
4389
+ const remote = ctx.get("remote.session");
4390
+ if (typeof remote?.create === "function") {
4391
+ const payload = cwd ? { cwd } : {};
4392
+ const receipt = rpcOk(await remote.create(slotArg(payload)));
4393
+ const created = receipt.value;
4394
+ if (receipt.ok && typeof created?.sessionId === "string") return {
4395
+ ok: true,
4396
+ id: created.sessionId
4397
+ };
4398
+ return { ok: false };
4399
+ }
4400
+ } catch {
4401
+ return { ok: false };
4402
+ }
4403
+ try {
4404
+ const id = await apiSessionCreate(cwd);
4405
+ if (id) return {
4406
+ ok: true,
4407
+ id
4408
+ };
4409
+ } catch {}
4410
+ return { ok: false };
4411
+ }
4412
+ /**
4413
+ * 解析会话输入门面;缺失时返回 undefined(动作侧守卫降级)。
4414
+ * @author ddj 2026年08月25号
4415
+ * @param ctx 客户端服务上下文
4416
+ * @param sessionId 会话 id
4417
+ * @returns SessionInput 或 undefined
4418
+ */
4419
+ function inputFor(ctx, sessionId) {
4420
+ if (!sessionId) return void 0;
4421
+ try {
4422
+ const sessions = ctx.get("sessions");
4423
+ const conversation = ctx.get("conversation");
4424
+ const actx = typeof sessions?.binding === "function" ? sessions.binding(sessionId)?.ctx : sessions?.scope?.(sessionId);
4425
+ const shell = actx && conversation?.input?.for?.(actx);
4426
+ return shell && typeof shell.insertReference === "function" ? shell : void 0;
4427
+ } catch {
4428
+ return;
4429
+ }
4430
+ }
4431
+ /** 取当前草稿快照(长度/版本/chip 列表);无输入门面时返回 null。
4432
+ * @author ddj 2026年08月25号 / 2026年09月11号 */
4433
+ function draftCursor(input) {
4434
+ try {
4435
+ const s = input?.state?.getSnapshot?.();
4436
+ if (!s) return null;
4437
+ return {
4438
+ draft: String(s.draft ?? ""),
4439
+ draftRev: Number(s.draftRev ?? 0),
4440
+ occurrences: Array.isArray(s.occurrences) ? s.occurrences : void 0
4441
+ };
4442
+ } catch {
4443
+ return null;
4444
+ }
4445
+ }
4446
+ /** 从 sessions 列表快照读会话工作区目录(与 index.ts 既有读取方式一致)。
4447
+ * @author ddj 2026年08月25号 */
4448
+ function cwdOf(ctx, sessionId) {
4449
+ if (!sessionId) return void 0;
4450
+ try {
4451
+ return ctx.get("sessions")?.list?.getSnapshot?.()?.byId?.[sessionId]?.cwd;
4452
+ } catch {
4453
+ return;
4454
+ }
4455
+ }
4456
+ /** 输入门面插入引用(拦截异常视作未应用,走降级)。
4457
+ * @author ddj 2026年08月25号 */
4458
+ function safeInsert(input, reference, span) {
4459
+ try {
4460
+ return input.insertReference(reference, span) === true;
4461
+ } catch {
4462
+ return false;
4463
+ }
4464
+ }
4465
+ /** 输入门面插入纯文本(官方 plain-text 通道;不存在或异常视作未应用)。
4466
+ * @author ddj 2026年09月11号 */
4467
+ function safeInsertText(input, text, span) {
4468
+ if (typeof input.insertText !== "function") return false;
4469
+ try {
4470
+ return input.insertText(text, span) === true;
4471
+ } catch {
4472
+ return false;
4473
+ }
4474
+ }
4475
+ /**
4476
+ * 读输入框当前光标(detect 坐标);门面缺失/异常返回 null(由调用方回落到文档末尾)。
4477
+ * @author ddj 2026年09月11号
4478
+ * @param input 输入门面
4479
+ * @returns caretSpan 结果或 null
4480
+ */
4481
+ function caretOf(input) {
4482
+ if (typeof input.caretSpan !== "function") return null;
4483
+ try {
4484
+ return input.caretSpan() ?? null;
4485
+ } catch {
4486
+ return null;
4487
+ }
4488
+ }
4489
+ /**
4490
+ * 创建「添加到对话」动作集(apply 阶段构建一次,随 props 传给 EditorView)。
4491
+ *
4492
+ * 插入位置 = 输入框**当前光标**(`caretSpan()`,detect 坐标;无光标提示时回落文档末尾),
4493
+ * 不再固定追加到最末尾。坐标一律用 **detect 投影**(见 detectEndOf 的缺陷说明);
4494
+ * 两条写入通道都失败时**不写任何内容**并返回 'failed' —— 旧实现在此处退回 `setDraft`
4495
+ * 整篇重建,会把既有 chip 全部销毁(连续添加多个引用即触发)。
4496
+ * @author ddj 2026年08月25号 / 2026年09月11号
4497
+ * @param ctx 客户端服务上下文(sessions + conversation)
4498
+ * @returns 动作集
4499
+ */
4500
+ function createAddToConversation(ctx) {
4501
+ const appendReference = async (sessionId, path, range, appearance) => {
4502
+ const input = inputFor(ctx, sessionId);
4503
+ if (!input) return "unavailable";
4504
+ const cur = draftCursor(input);
4505
+ if (!cur) return "unavailable";
4506
+ const { reference, mention } = buildFileRef(path, cwdOf(ctx, sessionId), range, appearance);
4507
+ const at = insertOffsetOf(caretOf(input), detectEndOf(cur));
4508
+ const span = {
4509
+ start: at,
4510
+ end: at,
4511
+ draftRev: cur.draftRev
4512
+ };
4513
+ if (safeInsert(input, reference, span)) return "ok";
4514
+ const pad = paddingAt(cur, at);
4515
+ if (safeInsertText(input, (pad.lead ? " " : "") + mention + (pad.tail ? " " : ""), span)) return "busy";
4516
+ return "failed";
4517
+ };
4518
+ /**
4519
+ * 追加纯文本(AI 深度分析任务注入):走官方 plain-text 通道插到当前光标。
4520
+ * 失败语义与 appendReference 一致:宁可不写也不整篇重建(保护既有 chip)。
4521
+ * @author ddj 2026年09月23号
4522
+ * @param sessionId 会话 id
4523
+ * @param text 待注入文本
4524
+ * @returns 注入结果(ok / unavailable / failed)
4525
+ */
4526
+ const appendText = async (sessionId, text) => {
4527
+ const input = inputFor(ctx, sessionId);
4528
+ if (!input) return "unavailable";
4529
+ const cur = draftCursor(input);
4530
+ if (!cur) return "unavailable";
4531
+ const at = insertOffsetOf(caretOf(input), detectEndOf(cur));
4532
+ return safeInsertText(input, text, {
4533
+ start: at,
4534
+ end: at,
4535
+ draftRev: cur.draftRev
4536
+ }) ? "ok" : "failed";
4537
+ };
4538
+ /**
4539
+ * 新建独立会话(02-deep-session-prompt:旁路任务不污染当前对话)。
4540
+ * 闭包持有真服务 ctx(面板 SidebarCtx 无 .get,会话操作必须经此门面);
4541
+ * 新版仅返回已登记身份,视图选择由宿主负责;旧版保留可用的 open/select。
4542
+ * @author ddj 2026年10月28号
4543
+ * @author ddj 2026年09月23号
4544
+ * @param cwd 新会话工作区目录(与当前会话同工作区)
4545
+ * @returns 新会话结果(失败供降级)
4546
+ */
4547
+ const startDraftSession = async (cwd) => {
4548
+ const result = await newDraftSession(ctx, cwd);
4549
+ if (typeof ctx.get("sessions")?.retain === "function") return result;
4550
+ if (result.ok && result.id) try {
4551
+ const sessions = ctx.get("sessions");
4552
+ sessions?.materializeScope?.(result.id);
4553
+ const title = "AI 深度分析 · " + (String(cwd ?? "").split(/[\\/]/).pop() || "SVN 变更");
4554
+ sessions?.manager?.rename?.(result.id, title);
4555
+ sessions?.manager?.select?.(result.id);
4556
+ sessions?.open?.(result.id);
4557
+ } catch {}
4558
+ return result;
4559
+ };
4560
+ /**
4561
+ * 发送任务给目标会话 agent(点击即发送方案:任务落地会话即非 blank,列表可见)。
4562
+ * 新版持有目标 Controller 到 prompt 结束;旧版按能力选择唯一通道,不跨通道重发。
4563
+ * @author ddj 2026年10月29号
4564
+ * @param sessionId 目标会话 id
4565
+ * @param text 任务文本
4566
+ * @returns 同一次调用的历史boolean语义,不能作为受理事实
4567
+ */
4568
+ const sendTask = async (sessionId, text) => (await sendTaskResult(sessionId, text)).legacyOk;
4569
+ /** @private @author ddj 2026年10月29号 @param sessionId Fixed target. @param text Task body. @returns Detailed evidence from exactly one selected transport. */
4570
+ async function sendTaskResult(sessionId, text) {
4571
+ if (!sessionId) return {
4572
+ status: "unavailable",
4573
+ legacyOk: false
4574
+ };
4575
+ try {
4576
+ const sessions = ctx.get("sessions");
4577
+ if (typeof sessions?.retain === "function") return await sendOwned(ctx, sessions, sessionId, text);
4578
+ return await sendLegacy(ctx, sessionId, text);
4579
+ } catch {
4580
+ return {
4581
+ status: "unavailable",
4582
+ legacyOk: false
4583
+ };
4584
+ }
4585
+ }
4586
+ return {
4587
+ appendReference,
4588
+ appendText,
4589
+ startDraftSession,
4590
+ sendTask,
4591
+ sendTaskResult
4592
+ };
4593
+ }
4594
+ //#endregion
4595
+ //#region src/client/openFlow.ts
4596
+ /**
4597
+ * dsh-vscode-mode client — 深链打开规则引擎(资源管理器右键 / Unity 外部编辑器落地)。
4598
+ * 规则:文件夹 ⊂ 某已注册工作区 → 页面内弹窗二选一(使用最近的工作区 / 新建工作区);
4599
+ * 文件夹不在任何工作区 → 直达最近对话所在工作区新增;文件 → 最近对话 + 编辑器展开
4600
+ * (无任何会话 → 以文件父目录注册工作区引导创建)。首路径驱动规则,其余路径:文件进
4601
+ * 编辑器、文件夹补引用。纯函数可单测;编排依赖经 OpenDeps 注入(mock 可测)。
4602
+ * 作者 ddj 2026-09-07
4603
+ */
4604
+ /** 等列表/输入就绪的节奏。 */
4605
+ const READY_TIMEOUT_MS = 15e3;
4606
+ const READY_TICK_MS = 300;
4607
+ const REF_RETRY_MAX = 20;
4608
+ const REF_RETRY_MS = 300;
4609
+ /**
4610
+ * 路径规范化:`\→/`、小写、去尾分隔符(Windows 大小写不敏感前缀匹配用)。
4611
+ * @author ddj 2026年09月07号
4612
+ */
4613
+ function normPath(p) {
4614
+ return String(p ?? "").replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
4615
+ }
4616
+ /**
4617
+ * 目录包含判定:child === root 或 child 位于 root 之下。
4618
+ * @author ddj 2026年09月07号
4619
+ */
4620
+ function containsPath(root, child) {
4621
+ const r = normPath(root);
4622
+ const c = normPath(child);
4623
+ return c === r || c.startsWith(r + "/");
4624
+ }
4625
+ /**
4626
+ * 父目录(文件引导创建工作区用;根路径回退自身)。
4627
+ * @author ddj 2026年09月07号
4628
+ */
4629
+ function dirnameOf(p) {
4630
+ const norm = String(p ?? "").replace(/[\\/]+$/, "");
4631
+ const cut = Math.max(norm.lastIndexOf("/"), norm.lastIndexOf("\\"));
4632
+ return cut > 0 ? norm.slice(0, cut) : norm;
4633
+ }
4634
+ /**
4635
+ * 最近一次对话:排除子代理行,updatedAt 最新优先。
4636
+ * @author ddj 2026年09月07号
4637
+ */
4638
+ function recentSession(byId, ids) {
4639
+ return (ids ?? Object.keys(byId)).map((id) => byId[id]).filter((row) => row && row.origin !== "subagent").sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0];
4640
+ }
4641
+ /**
4642
+ * 文件夹命中的已注册工作区(前缀包含),updatedAt 新者优先。
4643
+ * @author ddj 2026年09月07号
4644
+ */
4645
+ function matchWorkspaces(items, folder) {
4646
+ return items.filter((ws) => containsPath(ws.path, folder));
4647
+ }
4648
+ /**
4649
+ * 规则分派:目录+命中工作区 → choose;目录未命中 → folderNew(以该文件夹为根注册新工作区,
4650
+ * 用户确认:一律取右键的文件夹本身);文件有会话 → fileRecent;文件无会话 → bootstrap;
4651
+ * 路径缺失 → abort。
4652
+ * @author ddj 2026年09月08号
4653
+ */
4654
+ function pickRule(kind, matched, hasSession) {
4655
+ if (kind === "missing") return "abort";
4656
+ if (kind === "directory") return matched ? "choose" : "folderNew";
4657
+ return hasSession ? "fileRecent" : "bootstrap";
4658
+ }
4659
+ /**
4660
+ * 深链打开编排入口:等列表就绪 → 判型 → 规则分派 → 编排 create/open/引用/编辑器。
4661
+ * @author ddj 2026年09月07号
4662
+ * @param ctx 客户端根上下文(sessions/workspaces 服务)
4663
+ * @param params 深链参数(首路径驱动规则)
4664
+ * @param overrides 依赖覆盖(测试注入)
4665
+ */
4666
+ async function openDeepLink(ctx, params, overrides = {}) {
4667
+ const deps = realDeps(ctx, overrides);
4668
+ if (!await waitListsReady(deps)) {
4669
+ deps.notify("DSH 会话列表未就绪,请稍后重试");
4670
+ return false;
4671
+ }
4672
+ const primary = params.paths[0];
4673
+ const kind = await deps.stat(primary).catch(() => "missing");
4674
+ const sess = deps.sessions();
4675
+ const rule = pickRule(kind, matchWorkspaces(deps.workspaces().items, primary).length > 0, Boolean(recentSession(sess.byId, sess.ids)));
4676
+ try {
4677
+ if (rule === "abort") {
4678
+ deps.notify("路径不存在:" + primary);
4679
+ return false;
4680
+ }
4681
+ if (rule === "choose") await runChoose(deps, primary, params);
4682
+ else if (rule === "folderNew") await runFolderNew(deps, primary, params);
4683
+ else if (rule === "fileRecent") await runFileRecent(deps, primary, params);
4684
+ else await runFileBootstrap(deps, primary, params);
4685
+ return true;
4686
+ } catch (error) {
4687
+ deps.notify("打开失败:" + String(error?.message ?? error));
4688
+ return false;
4689
+ }
4690
+ }
4691
+ /** 等待会话与工作区列表就绪(上限 15s;超时按当前态判定)。 */
4692
+ async function waitListsReady(deps) {
4693
+ for (let waited = 0; waited < READY_TIMEOUT_MS; waited += READY_TICK_MS) {
4694
+ if (deps.sessions().ready && deps.workspaces().ready) return true;
4695
+ await sleep(deps, READY_TICK_MS);
4696
+ }
4697
+ return deps.sessions().ready && deps.workspaces().ready;
4698
+ }
4699
+ /** 规则 1:命中工作区 → 弹窗分派(取消即中止)。 */
4700
+ async function runChoose(deps, folder, params) {
4701
+ const best = bestMatch(deps, matchWorkspaces(deps.workspaces().items, folder));
4702
+ const choice = await deps.choose(best?.title ?? "工作区", folder);
4703
+ if (choice === null) throw new Error("用户取消打开");
4704
+ if (choice === "create") return runFolderNew(deps, folder, params);
4705
+ const sessionId = await spawnInWorkspace(deps, (best ?? deps.workspaces().items[0]).workspaceId, void 0);
4706
+ await settleRef(deps, sessionId, folder, "folder");
4707
+ await openChecked(deps, sessionId, null);
4708
+ await openRest(deps, sessionId, params, 1);
4709
+ }
4710
+ /** 规则 3a:文件 → 所属工作区优先(有会话打开其最近会话;无会话新建 + 文件引用);无匹配 → 最近对话。 */
4711
+ async function runFileRecent(deps, file, params) {
4712
+ const ws = pickFileWorkspace(deps, file);
4713
+ if (ws) {
4714
+ const anchor = recentSessionIn(deps, ws);
4715
+ if (anchor) {
4716
+ deps.openSession(anchor.id);
4717
+ await openChecked(deps, anchor.id, file, params.line, params.column);
4718
+ await openRest(deps, anchor.id, params, 1);
4719
+ return;
4720
+ }
4721
+ const sessionId = await spawnInWorkspace(deps, ws.workspaceId, void 0);
4722
+ await settleRef(deps, sessionId, file, "file");
4723
+ await openChecked(deps, sessionId, file, params.line, params.column);
4724
+ await openRest(deps, sessionId, params, 1);
4725
+ return;
4726
+ }
4727
+ const anchor = recentSession(deps.sessions().byId, deps.sessions().ids);
4728
+ if (!anchor) return runFileBootstrap(deps, file, params);
4729
+ deps.openSession(anchor.id);
4730
+ await openChecked(deps, anchor.id, file, params.line, params.column);
4731
+ await openRest(deps, anchor.id, params, 1);
4732
+ }
4733
+ /**
4734
+ * 文件所属工作区:前缀命中的工作区里,优先取有会话且最近活跃的;都无会话取路径最具体的。
4735
+ * @author ddj 2026年09月08号
4736
+ */
4737
+ function pickFileWorkspace(deps, file) {
4738
+ const matches = matchWorkspaces(deps.workspaces().items, file);
4739
+ if (!matches.length) return void 0;
4740
+ const withSessions = matches.filter((ws) => (ws.sessionIds ?? []).some((id) => Boolean(deps.sessions().byId[id])));
4741
+ if (!withSessions.length) return [...matches].sort((a, b) => normPath(b.path).length - normPath(a.path).length)[0];
4742
+ return withSessions.sort((a, b) => latestSessionAt(deps, b) - latestSessionAt(deps, a))[0];
4743
+ }
4744
+ /** 工作区内会话的最近活跃时间(无成员 → 0)。 */
4745
+ function latestSessionAt(deps, ws) {
4746
+ const byId = deps.sessions().byId;
4747
+ return (ws.sessionIds ?? []).reduce((max, id) => Math.max(max, byId[id]?.updatedAt ?? 0), 0);
4748
+ }
4749
+ /** 工作区内最近一次会话(排除子代理与不在列表中的陈旧行)。 */
4750
+ function recentSessionIn(deps, ws) {
4751
+ const byId = deps.sessions().byId;
4752
+ return (ws.sessionIds ?? []).map((id) => byId[id]).filter((row) => row && row.origin !== "subagent").sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0];
4753
+ }
4754
+ /** 规则 3b:文件 + 无任何会话 → 父目录注册工作区 + 新对话 + 文件引用 + 编辑器打开。 */
4755
+ async function runFileBootstrap(deps, file, params) {
4756
+ const sessionId = await spawnInWorkspace(deps, (await deps.createWorkspace(dirnameOf(file))).workspaceId, void 0);
4757
+ await settleRef(deps, sessionId, file, "file");
4758
+ await openChecked(deps, sessionId, file, params.line, params.column);
4759
+ await openRest(deps, sessionId, params, 1);
4760
+ }
4761
+ /** 规则 1.2:以文件夹注册新工作区 + 新对话 + 文件夹引用 + 编辑页。 */
4762
+ async function runFolderNew(deps, folder, params) {
4763
+ const sessionId = await spawnInWorkspace(deps, (await deps.createWorkspace(folder)).workspaceId, void 0);
4764
+ await settleRef(deps, sessionId, folder, "folder");
4765
+ await openChecked(deps, sessionId, null);
4766
+ await openRest(deps, sessionId, params, 1);
4767
+ }
4768
+ /** 在指定工作区(或回退 cwd)新建对话并置为当前;返回新会话 id。 */
4769
+ async function spawnInWorkspace(deps, workspaceId, fallbackCwd) {
4770
+ const sessionId = await deps.createSession(workspaceId ? { workspaceId } : fallbackCwd ? { cwd: fallbackCwd } : {});
4771
+ deps.openSession(sessionId);
4772
+ return sessionId;
4773
+ }
4774
+ /** 命中工作区集中「最近」的一个(其成员会话 updatedAt 最新者优先)。 */
4775
+ function bestMatch(deps, matches) {
4776
+ const byId = deps.sessions().byId;
4777
+ const score = (ws) => {
4778
+ const sessionLatest = (ws.sessionIds ?? []).reduce((max, id) => Math.max(max, byId[id]?.updatedAt ?? 0), 0);
4779
+ return Math.max(sessionLatest, Date.parse(ws.updatedAt ?? "") || 0);
4780
+ };
4781
+ return [...matches].sort((a, b) => score(b) - score(a))[0];
4782
+ }
4783
+ /** 引用插入轮询:输入门面就绪前每 300ms 重试(appendReference 忙态已降级视为成功)。 */
4784
+ async function settleRef(deps, sessionId, path, appearance) {
4785
+ for (let attempt = 0; attempt < REF_RETRY_MAX; attempt++) {
4786
+ if (await deps.reference(sessionId, path, appearance)) return;
4787
+ await sleep(deps, REF_RETRY_MS);
4788
+ }
4789
+ throw new Error("引用插入未完成");
4790
+ }
4791
+ /** 其余路径:文件进编辑器(步进),文件夹补文件夹引用。 */
4792
+ async function openRest(deps, sessionId, params, from) {
4793
+ const rest = params.paths.slice(from);
4794
+ for (const path of rest) {
4795
+ const kind = await deps.stat(path).catch(() => "missing");
4796
+ if (kind === "file") await openChecked(deps, sessionId ?? void 0, path);
4797
+ else if (kind === "directory" && sessionId) await settleRef(deps, sessionId, path, "folder");
4798
+ else throw new Error("路径不存在:" + path);
4799
+ }
4800
+ }
4801
+ /** 可注入 sleep。 */
4802
+ function sleep(deps, ms) {
4803
+ return new Promise((resolve) => deps.schedule(resolve, ms));
4804
+ }
4805
+ /** 真实依赖装配(全部来自 ctx 注入服务 + 既有 RPC/弹窗/编辑器入口)。 */
4806
+ function realDeps(ctx, overrides = {}) {
4807
+ const service = (name) => {
4808
+ const get = ctx?.get;
4809
+ return typeof get === "function" ? get.call(ctx, name) : void 0;
4810
+ };
4811
+ const add = createAddToConversation(ctx);
4812
+ return {
4813
+ stat: async (path) => {
4814
+ const result = await rpc("edrv.externalStat", { path });
4815
+ return result.ok ? result.kind : "missing";
4816
+ },
4817
+ workspaces: () => {
4818
+ const snap = service("workspaces")?.list?.getSnapshot?.() ?? {};
4819
+ return {
4820
+ items: snap.items ?? [],
4821
+ ready: snap.phase === "ready"
4822
+ };
4823
+ },
4824
+ sessions: () => {
4825
+ const snap = service("sessions")?.list?.getSnapshot?.() ?? {};
4826
+ return {
4827
+ ids: snap.ids ?? [],
4828
+ byId: snap.byId ?? {},
4829
+ ready: snap.phase !== "pending"
4830
+ };
4831
+ },
4832
+ createSession: async (opts) => {
4833
+ const sessions = service("sessions");
4834
+ if (typeof sessions?.create === "function") try {
4835
+ const created = await sessions.create(opts);
4836
+ const id = typeof created === "string" && created ? created : typeof created?.id === "string" ? created.id : typeof created?.sessionId === "string" ? created.sessionId : "";
4837
+ if (id) return id;
4838
+ } catch {}
4839
+ const draft = await newDraftSession(ctx, opts.cwd);
4840
+ if (draft.ok && draft.id) return draft.id;
4841
+ throw new Error("新建会话失败");
4842
+ },
4843
+ createWorkspace: async (path) => {
4844
+ const ws = await service("workspaces").create({ path });
4845
+ return {
4846
+ workspaceId: ws.workspaceId,
4847
+ title: ws.title
4848
+ };
4849
+ },
4850
+ openSession: (id) => {
4851
+ try {
4852
+ const sessions = service("sessions");
4853
+ sessions?.materializeScope?.(id);
4854
+ sessions?.manager?.select?.(id);
4855
+ sessions?.open?.(id);
4856
+ } catch {}
4857
+ },
4858
+ reference: async (sessionId, path, appearance) => {
4859
+ const outcome = await add.appendReference(sessionId, path, void 0, appearance);
4860
+ return outcome === "ok" || outcome === "busy";
4861
+ },
4862
+ choose: (title, folder) => chooseWorkspace(title, folder),
4863
+ openEditor: requestOpen,
4864
+ schedule: (fn, ms) => setTimeout(fn, ms),
4865
+ notify: (text) => toastDom(text),
4866
+ ...overrides
4867
+ };
4868
+ }
4869
+ /** @private @author ddj 2026年09月28号
4870
+ * Await actual editor completion and propagate failure to the request result.
4871
+ * @param deps Open dependencies.
4872
+ * @param sessionId Target session.
4873
+ * @param path Target file or editor mount.
4874
+ * @param line Optional line.
4875
+ * @param column Optional column.
4876
+ */
4877
+ async function openChecked(deps, sessionId, path, line, column) {
4878
+ if (!await deps.openEditor(path, line, column, sessionId)) throw new Error("编辑器打开未完成:" + (path ?? "工作区"));
4879
+ }
4880
+ //#endregion
4881
+ //#region src/client/externalOpen.ts
4882
+ /**
4883
+ * dsh-vscode-mode client — 外部深链落地(Windows 右键菜单 / Unity 外部编辑器)。
4884
+ * 启动时解析 location.search 的 edrvOpen 深链:跨源 referrer 守卫防外部网页诱导;
4885
+ * 处理完剥离 URL 参数防刷新重开;执行委托 openFlow 打开规则引擎(工作区/对话智能路由)。
4886
+ * 另挂 3s 移交轮询(兼活跃心跳):launcher 投递的打开请求由已打开页面就地执行,避免重复开新页。
4887
+ * 作者 ddj 2026-09-07 / 2026-09-08
4888
+ */
4889
+ /** toast 自隐时长。 */
4890
+ const TOAST_MS = 5e3;
4891
+ /** 深链移交轮询周期(兼活跃心跳)。 */
4892
+ const HANDOFF_POLL_MS = 3e3;
4893
+ /**
4894
+ * 页面隐藏时的降频倍数(3s × 5 = 15s)。
4895
+ * 必须仍短于 host 租约(src/externalHandoff.ts 的 LEASE_MS = 30s):心跳一旦超期,
4896
+ * host 判定该页面已死,launcher 会重复开新页——比多几次 RPC 更糟。
4897
+ */
4898
+ const HIDDEN_POLL_EVERY = 5;
4899
+ /**
4900
+ * 装配外部深链:深链参数缺失时仅保持移交轮询(零额外开销)。
4901
+ * @author ddj 2026年09月07号
4902
+ * @param ctx 客户端根上下文(sessions/workspaces 服务)
4903
+ * @param options 可注入选项
4904
+ */
4905
+ function setupExtOpen(ctx, options = {}) {
4906
+ const dispose = startPresencePoll(ctx);
4907
+ const params = parseOpenParams(options.search ?? (typeof location !== "undefined" ? location.search : ""));
4908
+ if (!params) return dispose;
4909
+ const referrer = options.referrer ?? (typeof document !== "undefined" ? document.referrer : "");
4910
+ if (!referrerAllowed(referrer, options.origin ?? (typeof location !== "undefined" ? location.origin : ""))) {
4911
+ log$2.warn("已忽略跨源深链请求(referrer=" + referrer + ")");
4912
+ return dispose;
4913
+ }
4914
+ stripCurrentUrl();
4915
+ openDeepLink(ctx, params).catch((error) => {
4916
+ log$2.warn("深链打开失败:" + String(error));
4917
+ toastDom("深链打开失败:" + String(error?.message ?? error));
4918
+ });
4919
+ return dispose;
4920
+ }
4921
+ /**
4922
+ * 深链移交轮询:可见时每 3s 向 host 领取待打开请求(兼活跃心跳,launcher 据此决定是否开新页);
4923
+ * 页面隐藏时降到 15s 一次——无人看页面时移交不必保持 3s 精度,但心跳不能停(超期会让 host 判页面已死)。
4924
+ * 页面级单例(window 标记防 HMR 重复);领取到即执行打开规则。
4925
+ * @author ddj 2026年09月08号 / 2026年10月15号
4926
+ * @param ctx 客户端根上下文
4927
+ */
4928
+ function startPresencePoll(ctx) {
4929
+ if (typeof window === "undefined") return () => {};
4930
+ const marker = window;
4931
+ if (typeof marker.__edrvExtPoll === "function") marker.__edrvExtPoll();
4932
+ const generatedId = createRequestId();
4933
+ if (!generatedId) {
4934
+ log$2.warn("安全随机源不可用,已停用外部打开心跳");
4935
+ return () => {};
4936
+ }
4937
+ const clientId = generatedId;
4938
+ let disposed = false;
4939
+ let polling = false;
4940
+ let executing = false;
4941
+ let ticks = 0;
4942
+ /**
4943
+ * 页面隐藏时按倍数跳轮;可见时每次都跑并清零计数。
4944
+ * @author ddj 2026年10月15号
4945
+ * @returns 是否跳过本轮
4946
+ */
4947
+ function hiddenSkip() {
4948
+ if (typeof document === "undefined" || document.visibilityState !== "hidden") {
4949
+ ticks = 0;
4950
+ return false;
4951
+ }
4952
+ ticks += 1;
4953
+ return ticks % HIDDEN_POLL_EVERY !== 0;
4954
+ }
4955
+ /** @private @author ddj 2026年09月28号 Open all requested paths before acknowledging this exact lease. */
4956
+ async function execute(open) {
4957
+ executing = true;
4958
+ try {
4959
+ const success = await openDeepLink(ctx, open);
4960
+ if (!disposed) await rpc("edrv.external.ack", {
4961
+ token: open.token,
4962
+ leaseId: open.leaseId,
4963
+ clientId,
4964
+ success,
4965
+ error: success ? void 0 : "编辑器打开失败或取消"
4966
+ });
4967
+ } finally {
4968
+ executing = false;
4969
+ }
4970
+ }
4971
+ /** @private @author ddj 2026年09月28号 Poll while opening to renew the lease without accepting a second request. */
4972
+ async function tick() {
4973
+ if (disposed || polling) return;
4974
+ polling = true;
4975
+ try {
4976
+ const result = await rpc("edrv.external.pending", {
4977
+ clientId,
4978
+ busy: executing
4979
+ });
4980
+ if (!disposed && result.ok && result.open && !executing) execute(result.open).catch(() => {});
4981
+ } finally {
4982
+ polling = false;
4983
+ }
4984
+ }
4985
+ const timer = window.setInterval(() => {
4986
+ if (hiddenSkip()) return;
4987
+ tick().catch(() => {});
4988
+ }, HANDOFF_POLL_MS);
4989
+ /** @private @author ddj 2026年09月28号 Stop heartbeats and fail outstanding editor waits on plugin disposal. */
4990
+ function dispose() {
4991
+ disposed = true;
4992
+ window.clearInterval(timer);
4993
+ disposeOpenWait();
4994
+ if (marker.__edrvExtPoll === dispose) delete marker.__edrvExtPoll;
4995
+ }
4996
+ marker.__edrvExtPoll = dispose;
4997
+ tick().catch(() => {});
4998
+ return dispose;
4999
+ }
5000
+ /**
5001
+ * 跨源守卫:referrer 为空(launcher/Unity 直开)或同源 → 允许。
5002
+ * @author ddj 2026年09月07号
5003
+ * @param referrer 文档 referrer
5004
+ * @param origin 当前页面源
5005
+ * @returns 是否允许处理深链
5006
+ */
5007
+ function referrerAllowed(referrer, origin) {
5008
+ if (!referrer) return true;
5009
+ try {
5010
+ return new URL(referrer).origin === origin;
5011
+ } catch {
5012
+ return false;
5013
+ }
5014
+ }
5015
+ /**
5016
+ * 剥离 URL 中的深链参数(保留其余参数与 hash,防刷新重开)。
5017
+ * @author ddj 2026年09月07号
5018
+ * @param href 完整 URL
5019
+ * @returns 清理后的 URL(pathname+search+hash)
5020
+ */
5021
+ function stripOpenHref(href) {
5022
+ const url = new URL(href);
5023
+ for (const key of EDRV_PARAM_KEYS) url.searchParams.delete(key);
5024
+ return url.pathname + url.search + url.hash;
5025
+ }
5026
+ /** 清理当前页面 URL(best-effort,失败不阻塞打开)。 */
5027
+ function stripCurrentUrl() {
5028
+ try {
5029
+ if (typeof location === "undefined" || typeof history === "undefined") return;
5030
+ const next = stripOpenHref(location.href);
5031
+ if (next !== location.pathname + location.search + location.hash) history.replaceState(null, "", next);
5032
+ } catch {}
5033
+ }
5034
+ /**
5035
+ * 轻量 toast(编辑器未挂载也能提示;5s 自隐;openFlow 通知与错误呈现共用)。
5036
+ * @author ddj 2026年09月07号
5037
+ * @param text 提示文本
5038
+ */
5039
+ function toastDom(text) {
5040
+ if (typeof document === "undefined") return;
5041
+ const toast = document.createElement("div");
5042
+ toast.textContent = text;
5043
+ toast.setAttribute("style", [
5044
+ "position:fixed",
5045
+ "left:50%",
5046
+ "bottom:28px",
5047
+ "transform:translateX(-50%)",
5048
+ "z-index:99999",
5049
+ "padding:8px 14px",
5050
+ "border-radius:8px",
5051
+ "max-width:80vw",
5052
+ "background:#2a2a2a",
5053
+ "color:#ddd",
5054
+ "border:1px solid #444",
5055
+ "font-size:12px",
5056
+ "box-shadow:0 4px 16px rgba(0,0,0,.35)"
5057
+ ].join(";"));
5058
+ document.body?.appendChild(toast);
5059
+ setTimeout(() => toast.remove(), TOAST_MS);
5060
+ }
5061
+ //#endregion
3724
5062
  //#region src/client/ui/ReviewAllConfirm.ts
3725
5063
  /**
3726
5064
  * @public
@@ -3731,7 +5069,7 @@ window.__ModuleLoader__.load({
3731
5069
  */
3732
5070
  function ReviewAllConfirm(props) {
3733
5071
  const action = props.reject ? "Undo All" : "Keep All";
3734
- const warning = props.reject ? "撤销会改写磁盘;逐项安全校验,遇到冲突停止,已完成项不会自动恢复。" : "只标记待审修改为保留,不改写文件。";
5072
+ const warning = props.reject ? "撤销会改写磁盘;逐项安全校验,遇到冲突停止,已完成项不会自动恢复。" : "只标记待审修改为保留,不改写文件;个别条目若已变化或需在活跃端判定会被跳过并留在列表里,可逐个处理。";
3735
5073
  const footer = react$1.default.createElement(react$1.default.Fragment, null, react$1.default.createElement(_deepseek_ai_dsh_client_ui_primitives.Button, {
3736
5074
  variant: "ghost",
3737
5075
  onClick: props.onCancel,
@@ -4214,6 +5552,34 @@ window.__ModuleLoader__.load({
4214
5552
  return !!page?.overview?.coveredPaths.some((candidate) => diffPathKey(candidate) === key);
4215
5553
  }
4216
5554
  /**
5555
+ * @author ddj 2026年11月3号
5556
+ * @description: 本页待审条目占用的路径键:路径本身与其身份各一份。
5557
+ * @param page 审查页
5558
+ * @returns 待审覆盖的路径键集合
5559
+ */
5560
+ function pendingCoverKeys(page) {
5561
+ const keys = /* @__PURE__ */ new Set();
5562
+ for (const item of page?.items ?? []) {
5563
+ if (!item.id.startsWith("pending:")) continue;
5564
+ keys.add(diffPathKey(item.currentPath || item.path));
5565
+ keys.add(diffPathKey(item.id.slice(8)));
5566
+ }
5567
+ return keys;
5568
+ }
5569
+ /**
5570
+ * @public
5571
+ * @author ddj 2026年11月3号
5572
+ * @description: 取本页"已有权威通道在管"的路径键:官方汇总覆盖集 + 待审条目。
5573
+ * 编辑器与差异汇总必须同口径,否则同一改动在列表里被压掉、在编辑器里却仍然画得出来并可以点。
5574
+ * @param page 审查页
5575
+ * @returns 路径比较键集合
5576
+ */
5577
+ function coveredPathKeys(page) {
5578
+ const keys = pendingCoverKeys(page);
5579
+ for (const path of page?.overview?.coveredPaths ?? []) keys.add(diffPathKey(path));
5580
+ return keys;
5581
+ }
5582
+ /**
4217
5583
  * @author ddj 2026年10月30号
4218
5584
  * @description: 取出本页待审条目(id 带待审前缀)并折算成文件行。
4219
5585
  * @param page 审查页
@@ -4221,12 +5587,9 @@ window.__ModuleLoader__.load({
4221
5587
  */
4222
5588
  function pendingRowsOf(page) {
4223
5589
  const rows = [];
4224
- const covered = /* @__PURE__ */ new Set();
4225
5590
  for (const item of page?.items ?? []) {
4226
5591
  if (!item.id.startsWith("pending:")) continue;
4227
5592
  const path = item.currentPath || item.path;
4228
- covered.add(diffPathKey(path));
4229
- covered.add(diffPathKey(item.id.slice(8)));
4230
5593
  rows.push({
4231
5594
  path,
4232
5595
  label: item.display || path,
@@ -4239,7 +5602,7 @@ window.__ModuleLoader__.load({
4239
5602
  }
4240
5603
  return {
4241
5604
  rows,
4242
- covered
5605
+ covered: pendingCoverKeys(page)
4243
5606
  };
4244
5607
  }
4245
5608
  /** @public @author ddj 2026年10月26号 @param records Tool-call history. @param page Official file navigation, never only its first 50 rows. @returns Pending first, then official; legacy paths remain explicitly labelled. */
@@ -145912,10 +147275,10 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
145912
147275
  var require_useMergeRef = /* @__PURE__ */ __commonJSMin(((exports) => {
145913
147276
  Object.defineProperty(exports, "__esModule", { value: true });
145914
147277
  exports.useMergeRefs = void 0;
145915
- var React$69 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports)).__importStar(require("react"));
147278
+ var React$68 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports)).__importStar(require("react"));
145916
147279
  var assignRef_1 = require_assignRef();
145917
147280
  var useRef_1 = require_useRef();
145918
- var useIsomorphicLayoutEffect = typeof window !== "undefined" ? React$69.useLayoutEffect : React$69.useEffect;
147281
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? React$68.useLayoutEffect : React$68.useEffect;
145919
147282
  var currentValues = /* @__PURE__ */ new WeakMap();
145920
147283
  /**
145921
147284
  * Merges two or more refs together providing a single interface to set their value
@@ -146195,7 +147558,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146195
147558
  Object.defineProperty(exports, "__esModule", { value: true });
146196
147559
  exports.sidecar = void 0;
146197
147560
  var tslib_1 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports));
146198
- var React$68 = tslib_1.__importStar(require("react"));
147561
+ var React$67 = tslib_1.__importStar(require("react"));
146199
147562
  var hook_1 = require_hook$1();
146200
147563
  function sidecar(importer, errorComponent) {
146201
147564
  var ErrorCase = function() {
@@ -146204,7 +147567,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146204
147567
  return function Sidecar(props) {
146205
147568
  var _a = (0, hook_1.useSidecar)(importer, props.sideCar), Car = _a[0];
146206
147569
  if (_a[1] && errorComponent) return ErrorCase;
146207
- return Car ? React$68.createElement(Car, tslib_1.__assign({}, props)) : null;
147570
+ return Car ? React$67.createElement(Car, tslib_1.__assign({}, props)) : null;
146208
147571
  };
146209
147572
  }
146210
147573
  exports.sidecar = sidecar;
@@ -146319,7 +147682,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146319
147682
  Object.defineProperty(exports, "__esModule", { value: true });
146320
147683
  exports.renderCar = void 0;
146321
147684
  var tslib_1 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports));
146322
- var React$67 = tslib_1.__importStar(require("react"));
147685
+ var React$66 = tslib_1.__importStar(require("react"));
146323
147686
  var react_1 = require("react");
146324
147687
  function renderCar(WrappedComponent, defaults) {
146325
147688
  function State(_a) {
@@ -146332,9 +147695,9 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146332
147695
  });
146333
147696
  return null;
146334
147697
  }, []);
146335
- return React$67.createElement(WrappedComponent, tslib_1.__assign({}, props, { children: renderTarget }));
147698
+ return React$66.createElement(WrappedComponent, tslib_1.__assign({}, props, { children: renderTarget }));
146336
147699
  }
146337
- var Children = React$67.memo(function(_a) {
147700
+ var Children = React$66.memo(function(_a) {
146338
147701
  var stateRef = _a.stateRef, defaultState = _a.defaultState, children = _a.children;
146339
147702
  var _b = (0, react_1.useState)(defaultState.current), state = _b[0], setState = _b[1];
146340
147703
  (0, react_1.useEffect)(function() {
@@ -146345,14 +147708,14 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146345
147708
  return true;
146346
147709
  });
146347
147710
  return function Combiner(props) {
146348
- var defaultState = React$67.useRef(defaults(props));
146349
- var ref = React$67.useRef(function(state) {
147711
+ var defaultState = React$66.useRef(defaults(props));
147712
+ var ref = React$66.useRef(function(state) {
146350
147713
  return defaultState.current = state;
146351
147714
  });
146352
- return React$67.createElement(React$67.Fragment, null, React$67.createElement(State, {
147715
+ return React$66.createElement(React$66.Fragment, null, React$66.createElement(State, {
146353
147716
  stateRef: ref,
146354
147717
  props
146355
- }), React$67.createElement(Children, {
147718
+ }), React$66.createElement(Children, {
146356
147719
  stateRef: ref,
146357
147720
  defaultState,
146358
147721
  children: props.children
@@ -146367,13 +147730,13 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146367
147730
  Object.defineProperty(exports, "__esModule", { value: true });
146368
147731
  exports.exportSidecar = void 0;
146369
147732
  var tslib_1 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports));
146370
- var React$66 = tslib_1.__importStar(require("react"));
147733
+ var React$65 = tslib_1.__importStar(require("react"));
146371
147734
  var SideCar = function(_a) {
146372
147735
  var sideCar = _a.sideCar, rest = tslib_1.__rest(_a, ["sideCar"]);
146373
147736
  if (!sideCar) throw new Error("Sidecar: please provide `sideCar` property to import the right car");
146374
147737
  var Target = sideCar.read();
146375
147738
  if (!Target) throw new Error("Sidecar medium not found");
146376
- return React$66.createElement(Target, tslib_1.__assign({}, rest));
147739
+ return React$65.createElement(Target, tslib_1.__assign({}, rest));
146377
147740
  };
146378
147741
  SideCar.isSideCarExport = true;
146379
147742
  function exportSidecar(medium, exported) {
@@ -146449,7 +147812,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146449
147812
  Object.defineProperty(exports, "__esModule", { value: true });
146450
147813
  exports.RemoveScroll = void 0;
146451
147814
  var tslib_1 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports));
146452
- var React$65 = tslib_1.__importStar(require("react"));
147815
+ var React$64 = tslib_1.__importStar(require("react"));
146453
147816
  var constants_1 = require_constants();
146454
147817
  var use_callback_ref_1 = require_es5$5();
146455
147818
  var medium_1 = require_medium();
@@ -146457,9 +147820,9 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146457
147820
  /**
146458
147821
  * Removes scrollbar from the page and contain the scroll within the Lock
146459
147822
  */
146460
- var RemoveScroll = React$65.forwardRef(function(props, parentRef) {
146461
- var ref = React$65.useRef(null);
146462
- var _a = React$65.useState({
147823
+ var RemoveScroll = React$64.forwardRef(function(props, parentRef) {
147824
+ var ref = React$64.useRef(null);
147825
+ var _a = React$64.useState({
146463
147826
  onScrollCapture: nothing,
146464
147827
  onWheelCapture: nothing,
146465
147828
  onTouchMoveCapture: nothing
@@ -146482,7 +147845,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146482
147845
  var SideCar = sideCar;
146483
147846
  var containerRef = (0, use_callback_ref_1.useMergeRefs)([ref, parentRef]);
146484
147847
  var containerProps = tslib_1.__assign(tslib_1.__assign({}, rest), callbacks);
146485
- return React$65.createElement(React$65.Fragment, null, enabled && React$65.createElement(SideCar, {
147848
+ return React$64.createElement(React$64.Fragment, null, enabled && React$64.createElement(SideCar, {
146486
147849
  sideCar: medium_1.effectCar,
146487
147850
  removeScrollBar,
146488
147851
  shards,
@@ -146493,7 +147856,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146493
147856
  allowPinchZoom: !!allowPinchZoom,
146494
147857
  lockRef: ref,
146495
147858
  gapMode
146496
- }), forwardProps ? React$65.cloneElement(React$65.Children.only(children), tslib_1.__assign(tslib_1.__assign({}, containerProps), { ref: containerRef })) : React$65.createElement(Container, tslib_1.__assign({}, containerProps, {
147859
+ }), forwardProps ? React$64.cloneElement(React$64.Children.only(children), tslib_1.__assign(tslib_1.__assign({}, containerProps), { ref: containerRef })) : React$64.createElement(Container, tslib_1.__assign({}, containerProps, {
146497
147860
  className,
146498
147861
  ref: containerRef
146499
147862
  }), children));
@@ -146572,7 +147935,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146572
147935
  var require_hook = /* @__PURE__ */ __commonJSMin(((exports) => {
146573
147936
  Object.defineProperty(exports, "__esModule", { value: true });
146574
147937
  exports.styleHookSingleton = void 0;
146575
- var React$64 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports)).__importStar(require("react"));
147938
+ var React$63 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports)).__importStar(require("react"));
146576
147939
  var singleton_1 = require_singleton();
146577
147940
  /**
146578
147941
  * creates a hook to control style singleton
@@ -146586,7 +147949,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146586
147949
  var styleHookSingleton = function() {
146587
147950
  var sheet = (0, singleton_1.stylesheetSingleton)();
146588
147951
  return function(styles, isDynamic) {
146589
- React$64.useEffect(function() {
147952
+ React$63.useEffect(function() {
146590
147953
  sheet.add(styles);
146591
147954
  return function() {
146592
147955
  sheet.remove();
@@ -146691,7 +148054,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146691
148054
  var require_component = /* @__PURE__ */ __commonJSMin(((exports) => {
146692
148055
  Object.defineProperty(exports, "__esModule", { value: true });
146693
148056
  exports.RemoveScrollBar = exports.useLockAttribute = exports.lockAttribute = void 0;
146694
- var React$63 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports)).__importStar(require("react"));
148057
+ var React$62 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports)).__importStar(require("react"));
146695
148058
  var react_style_singleton_1 = require_es5$2();
146696
148059
  var constants_1 = require_constants();
146697
148060
  var utils_1 = require_utils();
@@ -146711,7 +148074,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146711
148074
  return isFinite(counter) ? counter : 0;
146712
148075
  };
146713
148076
  var useLockAttribute = function() {
146714
- React$63.useEffect(function() {
148077
+ React$62.useEffect(function() {
146715
148078
  document.body.setAttribute(exports.lockAttribute, (getCurrentUseCounter() + 1).toString());
146716
148079
  return function() {
146717
148080
  var newCounter = getCurrentUseCounter() - 1;
@@ -146727,10 +148090,10 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146727
148090
  var RemoveScrollBar = function(_a) {
146728
148091
  var noRelative = _a.noRelative, noImportant = _a.noImportant, _b = _a.gapMode, gapMode = _b === void 0 ? "margin" : _b;
146729
148092
  (0, exports.useLockAttribute)();
146730
- var gap = React$63.useMemo(function() {
148093
+ var gap = React$62.useMemo(function() {
146731
148094
  return (0, utils_1.getGapWidth)(gapMode);
146732
148095
  }, [gapMode]);
146733
- return React$63.createElement(Style, { styles: getStyles(gap, !noRelative, gapMode, !noImportant ? "!important" : "") });
148096
+ return React$62.createElement(Style, { styles: getStyles(gap, !noRelative, gapMode, !noImportant ? "!important" : "") });
146734
148097
  };
146735
148098
  exports.RemoveScrollBar = RemoveScrollBar;
146736
148099
  }));
@@ -146892,7 +148255,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146892
148255
  Object.defineProperty(exports, "__esModule", { value: true });
146893
148256
  exports.RemoveScrollSideCar = exports.getDeltaXY = exports.getTouchXY = void 0;
146894
148257
  var tslib_1 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports));
146895
- var React$62 = tslib_1.__importStar(require("react"));
148258
+ var React$61 = tslib_1.__importStar(require("react"));
146896
148259
  var react_remove_scroll_bar_1 = require_es5$1();
146897
148260
  var react_style_singleton_1 = require_es5$2();
146898
148261
  var aggresiveCapture_1 = require_aggresiveCapture();
@@ -146917,16 +148280,16 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146917
148280
  var idCounter = 0;
146918
148281
  var lockStack = [];
146919
148282
  function RemoveScrollSideCar(props) {
146920
- var shouldPreventQueue = React$62.useRef([]);
146921
- var touchStartRef = React$62.useRef([0, 0]);
146922
- var activeAxis = React$62.useRef();
146923
- var id = React$62.useState(idCounter++)[0];
146924
- var Style = React$62.useState(react_style_singleton_1.styleSingleton)[0];
146925
- var lastProps = React$62.useRef(props);
146926
- React$62.useEffect(function() {
148283
+ var shouldPreventQueue = React$61.useRef([]);
148284
+ var touchStartRef = React$61.useRef([0, 0]);
148285
+ var activeAxis = React$61.useRef();
148286
+ var id = React$61.useState(idCounter++)[0];
148287
+ var Style = React$61.useState(react_style_singleton_1.styleSingleton)[0];
148288
+ var lastProps = React$61.useRef(props);
148289
+ React$61.useEffect(function() {
146927
148290
  lastProps.current = props;
146928
148291
  }, [props]);
146929
- React$62.useEffect(function() {
148292
+ React$61.useEffect(function() {
146930
148293
  if (props.inert) {
146931
148294
  document.body.classList.add("block-interactivity-".concat(id));
146932
148295
  var allow_1 = tslib_1.__spreadArray([props.lockRef.current], (props.shards || []).map(extractRef), true).filter(Boolean);
@@ -146945,7 +148308,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146945
148308
  props.lockRef.current,
146946
148309
  props.shards
146947
148310
  ]);
146948
- var shouldCancelEvent = React$62.useCallback(function(event, parent) {
148311
+ var shouldCancelEvent = React$61.useCallback(function(event, parent) {
146949
148312
  if ("touches" in event && event.touches.length === 2 || event.type === "wheel" && event.ctrlKey) return !lastProps.current.allowPinchZoom;
146950
148313
  var touch = (0, exports.getTouchXY)(event);
146951
148314
  var touchStart = touchStartRef.current;
@@ -146971,7 +148334,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146971
148334
  var cancelingAxis = activeAxis.current || currentAxis;
146972
148335
  return (0, handleScroll_1.handleScroll)(cancelingAxis, parent, event, cancelingAxis === "h" ? deltaX : deltaY, true);
146973
148336
  }, []);
146974
- var shouldPrevent = React$62.useCallback(function(_event) {
148337
+ var shouldPrevent = React$61.useCallback(function(_event) {
146975
148338
  var event = _event;
146976
148339
  if (!lockStack.length || lockStack[lockStack.length - 1] !== Style) return;
146977
148340
  var delta = "deltaY" in event ? (0, exports.getDeltaXY)(event) : (0, exports.getTouchXY)(event);
@@ -146991,7 +148354,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
146991
148354
  }
146992
148355
  }
146993
148356
  }, []);
146994
- var shouldCancel = React$62.useCallback(function(name, delta, target, should) {
148357
+ var shouldCancel = React$61.useCallback(function(name, delta, target, should) {
146995
148358
  var event = {
146996
148359
  name,
146997
148360
  delta,
@@ -147006,17 +148369,17 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
147006
148369
  });
147007
148370
  }, 1);
147008
148371
  }, []);
147009
- var scrollTouchStart = React$62.useCallback(function(event) {
148372
+ var scrollTouchStart = React$61.useCallback(function(event) {
147010
148373
  touchStartRef.current = (0, exports.getTouchXY)(event);
147011
148374
  activeAxis.current = void 0;
147012
148375
  }, []);
147013
- var scrollWheel = React$62.useCallback(function(event) {
148376
+ var scrollWheel = React$61.useCallback(function(event) {
147014
148377
  shouldCancel(event.type, (0, exports.getDeltaXY)(event), event.target, shouldCancelEvent(event, props.lockRef.current));
147015
148378
  }, []);
147016
- var scrollTouchMove = React$62.useCallback(function(event) {
148379
+ var scrollTouchMove = React$61.useCallback(function(event) {
147017
148380
  shouldCancel(event.type, (0, exports.getTouchXY)(event), event.target, shouldCancelEvent(event, props.lockRef.current));
147018
148381
  }, []);
147019
- React$62.useEffect(function() {
148382
+ React$61.useEffect(function() {
147020
148383
  lockStack.push(Style);
147021
148384
  props.setCallbacks({
147022
148385
  onScrollCapture: scrollWheel,
@@ -147036,7 +148399,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
147036
148399
  };
147037
148400
  }, []);
147038
148401
  var removeScrollBar = props.removeScrollBar, inert = props.inert;
147039
- return React$62.createElement(React$62.Fragment, null, inert ? React$62.createElement(Style, { styles: generateStyle(id) }) : null, removeScrollBar ? React$62.createElement(react_remove_scroll_bar_1.RemoveScrollBar, {
148402
+ return React$61.createElement(React$61.Fragment, null, inert ? React$61.createElement(Style, { styles: generateStyle(id) }) : null, removeScrollBar ? React$61.createElement(react_remove_scroll_bar_1.RemoveScrollBar, {
147040
148403
  noRelative: props.noRelative,
147041
148404
  gapMode: props.gapMode
147042
148405
  }) : null);
@@ -147068,11 +148431,11 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
147068
148431
  var require_Combination = /* @__PURE__ */ __commonJSMin(((exports) => {
147069
148432
  Object.defineProperty(exports, "__esModule", { value: true });
147070
148433
  var tslib_1 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports));
147071
- var React$61 = tslib_1.__importStar(require("react"));
148434
+ var React$60 = tslib_1.__importStar(require("react"));
147072
148435
  var UI_1 = require_UI();
147073
148436
  var sidecar_1 = tslib_1.__importDefault(require_sidecar());
147074
- var ReactRemoveScroll = React$61.forwardRef(function(props, ref) {
147075
- return React$61.createElement(UI_1.RemoveScroll, tslib_1.__assign({}, props, {
148437
+ var ReactRemoveScroll = React$60.forwardRef(function(props, ref) {
148438
+ return React$60.createElement(UI_1.RemoveScroll, tslib_1.__assign({}, props, {
147076
148439
  ref,
147077
148440
  sideCar: sidecar_1.default
147078
148441
  }));
@@ -375421,9 +376784,15 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
375421
376784
  */
375422
376785
  function resolveNavLine(params) {
375423
376786
  const line = (params ?? {})?.line;
375424
- 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;
375425
376788
  return Math.floor(line);
375426
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
+ }
375427
376796
  /** 逐段 component 解码(失败返回 null,对齐官方 parseFileAddress 的容错语义)。 */
375428
376797
  function decodeSegment(raw) {
375429
376798
  if (raw === void 0) return null;
@@ -375444,7 +376813,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
375444
376813
  */
375445
376814
  function parseOfficialFileAddress(address) {
375446
376815
  if (typeof address !== "string" || !address.startsWith("dsh-resource://file/")) return null;
375447
- const [scope, ...tail] = address.slice(20).split("/");
376816
+ const [scope, ...tail] = address.split(/[?#]/, 1)[0].slice(20).split("/");
375448
376817
  const kind = decodeSegment(scope);
375449
376818
  if (kind === "session") {
375450
376819
  const sessionId = decodeSegment(tail[0]);
@@ -375512,10 +376881,16 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
375512
376881
  * @param preview 可选 Markdown 渲染模式(行号导航优先源码)
375513
376882
  * @returns 转发是否成功(openTab 抛错返回 false,由调用方决定重试或兜底)
375514
376883
  */
375515
- function forwardToEditor(service, path, line, preview) {
376884
+ function forwardToEditor(service, path, line, preview, column) {
375516
376885
  if (!service || typeof service.openTab !== "function" || !path) return false;
375517
376886
  const params = { openPath: path };
375518
- 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;
375519
376894
  if (preview !== void 0) params.preview = preview;
375520
376895
  try {
375521
376896
  service.openTab(OFFICIAL_TAB_KIND, { params });
@@ -382739,6 +384114,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
382739
384114
  const SIDECAR_KEEP_TITLE = "标记这些历史改动为已看过:只改记录,不改写磁盘文件,也不影响官方审查。";
382740
384115
  /** 非官方 Undo 的作用域说明:走原有回滚校验写回磁盘,但不影响官方记录。 */
382741
384116
  const SIDECAR_UNDO_TITLE = "撤销这些历史改动(写回受回滚校验保护):只影响这份历史记录,不影响官方审查。";
384117
+ /** 非官方 Keep 之后的完成态:该通道 Keep 不写盘,不能沿用「已采纳」这种会让人以为改过文件的措辞。 */
384118
+ const SIDECAR_KEEP_DONE = "已标记为看过(未改动文件)";
382742
384119
  /** 当前端没有该会话时,差异栏里给中性说明而不是红色报错。 */
382743
384120
  const SESSION_DOWN_NOTICE = "这个会话没在当前端激活,官方审查数据暂时读不到;下面的历史改动仍可查看、Keep、Undo。";
382744
384121
  /**
@@ -384506,641 +385883,6 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
384506
385883
  localStorage.setItem(key, text);
384507
385884
  } catch (error) {}
384508
385885
  }
384509
- //#endregion
384510
- //#region src/client/addToConversation.ts
384511
- /**
384512
- * dsh-vscode-mode client — 把文件/选中内容注入会话对话输入框(composer draft)。
384513
- * 复用 DSH 官方输入门面:ctx.conversation.input.for(sessions.scope(sessionId)) → SessionInput。
384514
- * 引用 chip:insertReference(reference source,发送时序列化为 @path 文本);忙态降级为纯文本 setDraft。
384515
- * 纯函数部分 node 可单测。
384516
- * 作者 ddj 2026-08-25
384517
- */
384518
- /** 把追加引用结果映射为可读文案(ok 用 okText;busy 提示已降级纯文本;unavailable 提示不可用)。
384519
- * @author ddj 2026年09月03号 / 2026年09月11号
384520
- * @param outcome 追加结果状态
384521
- * @param okText 成功文案(如「已添加文件引用」/「已添加文件夹引用」)
384522
- * @returns 状态栏/通知用文案
384523
- */
384524
- function statusOfAdd(outcome, okText) {
384525
- if (outcome === "ok") return okText;
384526
- if (outcome === "busy") return okText + "(已降级纯文本)";
384527
- if (outcome === "failed") return "添加引用失败(输入框忙或未就绪,请重试)";
384528
- return "无法添加到对话(无会话或输入框不可用)";
384529
- }
384530
- /** DSH reference source 名(dsh-client-ui-reference 注册的 @file/@session 统一源)。 */
384531
- const REF_SOURCE = "reference";
384532
- /**
384533
- * 把 clipboard 投影的末尾折算成 **detect 投影**的末尾。
384534
- *
384535
- * ⚠️ 两个投影对 chip 的计长不同,混用会直接毁掉已插入的引用(真实缺陷,勿改回 `draft.length`):
384536
- * DSH 的 `$composerLayout()` 中 chip 在 detect 投影里只贡献 **1 个 U+FFFC**,
384537
- * 在 clipboard 投影里贡献 `clipboardText` 全文
384538
- * (`pushLeaf('chip', kid, '', kid.getTextContent())`)。
384539
- * 而 `insertReference(ref, span)` 的 span 走 detect 坐标并经 `selectSpan` 校验:
384540
- * `span.end > layout.detectLength` 时返回 null → 插入失败。
384541
- *
384542
- * 旧实现直接用 `draft.length`(clipboard 长度)当 detect 起点:草稿里一旦有 chip,
384543
- * 该值就超出 detect 长度 → 插入被拒 → 落入 `setDraft` 兜底,
384544
- * 而 `setDraft` 会整篇重建并剔除 U+FFFC(`text.replace(REFERENCE_PLACEHOLDER_RE, '')`),
384545
- * 于是**既有 chip 被销毁、引用退化为重复的纯文本**(即「连续添加多个引用显示异常」)。
384546
- *
384547
- * 换算:每个 chip 在 clipboard 中多算了 `length - 1`,逐个扣回即得 detect 末尾。
384548
- * 无 occurrences(旧版 DSH)时退化为 `draft.length`,与旧行为一致。
384549
- * @author ddj 2026年09月11号
384550
- * @param snapshot 输入快照
384551
- * @returns detect 投影中的文档末尾偏移
384552
- */
384553
- function detectEndOf(snapshot) {
384554
- const total = String(snapshot?.draft ?? "").length;
384555
- const occurrences = snapshot?.occurrences;
384556
- if (!Array.isArray(occurrences) || !occurrences.length) return total;
384557
- let clipped = 0;
384558
- for (const occurrence of occurrences) {
384559
- const length = Number(occurrence?.length ?? 0);
384560
- if (!Number.isFinite(length) || length <= 1) continue;
384561
- clipped += length - 1;
384562
- }
384563
- return Math.max(0, total - clipped);
384564
- }
384565
- /** chip 列表按 clipboard 偏移升序(官方已排序,此处防御性归一,丢弃非法项)。 */
384566
- function chipsOf(snapshot) {
384567
- const occurrences = snapshot?.occurrences;
384568
- if (!Array.isArray(occurrences) || !occurrences.length) return [];
384569
- const out = [];
384570
- for (const occurrence of occurrences) {
384571
- const offset = Number(occurrence?.offset ?? NaN);
384572
- const length = Number(occurrence?.length ?? NaN);
384573
- if (!Number.isFinite(offset) || !Number.isFinite(length) || length <= 1) continue;
384574
- out.push({
384575
- offset: Math.max(0, offset),
384576
- length
384577
- });
384578
- }
384579
- return out.sort((a, b) => a.offset - b.offset);
384580
- }
384581
- /**
384582
- * detect 偏移 → clipboard 偏移(paddingAt 用:draft 是 clipboard 投影,不能直接按下标取字符)。
384583
- * 每个位于该偏移之前的 chip 在 clipboard 中多占 `length − 1`,累计补上即可。
384584
- * @author ddj 2026年09月11号
384585
- * @param snapshot 输入快照
384586
- * @param detectOffset detect 投影偏移
384587
- * @returns clipboard 投影偏移(已夹到 draft 范围内)
384588
- */
384589
- function detectToClipboardOf(snapshot, detectOffset) {
384590
- const total = String(snapshot?.draft ?? "").length;
384591
- const target = Math.max(0, Math.min(Number(detectOffset) || 0, detectEndOf(snapshot)));
384592
- let shift = 0;
384593
- for (const chip of chipsOf(snapshot)) {
384594
- if (chip.offset - shift >= target) break;
384595
- shift += chip.length - 1;
384596
- }
384597
- return Math.max(0, Math.min(target + shift, total));
384598
- }
384599
- /**
384600
- * 插入点两侧是否需要补空格(仅纯文本通道需要;chip 由 facade 自行补尾随空格)。
384601
- *
384602
- * 在 detect 坐标的任意位置求值,故**末尾追加的行为与旧实现逐字符一致**
384603
- * (末尾前为非空白 → 前后各补一个空格;末尾前已是空白 → 只补尾随空格),
384604
- * 同时正确支持「插在文本中间」不再贴字。
384605
- * @author ddj 2026年09月11号
384606
- * @param snapshot 输入快照
384607
- * @param detectOffset detect 投影插入点
384608
- * @returns 是否需要前导/尾随空格
384609
- */
384610
- function paddingAt(snapshot, detectOffset) {
384611
- const draft = String(snapshot?.draft ?? "");
384612
- const at = detectToClipboardOf(snapshot, detectOffset);
384613
- const before = at > 0 ? draft.slice(at - 1, at) : "";
384614
- const after = at < draft.length ? draft.slice(at, at + 1) : "";
384615
- return {
384616
- lead: before !== "" && !/\s/.test(before),
384617
- tail: after === "" || !/\s/.test(after)
384618
- };
384619
- }
384620
- /**
384621
- * 解析插入点(detect 坐标):优先用输入框**当前光标/选区**(`caretSpan()`,
384622
- * 官方契约即 detect 坐标,无选区时回落文档末尾),并夹到 `[0, detectLength]`。
384623
- *
384624
- * 选区非塌缩时取 `end`(右边界)—— 插入而非替换,绝不删除用户已选中的内容。
384625
- * caret 缺失/非法(旧版 facade 无该方法、抛异常、NaN 等)一律回落到文档末尾。
384626
- * @author ddj 2026年09月11号
384627
- * @param caret caretSpan() 的返回值(可为 null/非法)
384628
- * @param detectLength detect 投影文档长度
384629
- * @returns detect 投影插入偏移
384630
- */
384631
- function insertOffsetOf(caret, detectLength) {
384632
- const max = Math.max(0, Number(detectLength) || 0);
384633
- if (!caret || typeof caret !== "object") return max;
384634
- const span = caret;
384635
- const raw = Number(span.end);
384636
- const value = Number.isFinite(raw) ? raw : Number(span.start);
384637
- if (!Number.isFinite(value)) return max;
384638
- return Math.max(0, Math.min(value, max));
384639
- }
384640
- /**
384641
- * 生成 DSH @file 语法引用串:cwd 相对化、\ → /、含空白时按 @"path" 语法加引号。
384642
- * cwd 外/无法相对化的路径回退原路径。
384643
- * @author ddj 2026年08月25号
384644
- * @param path 打开的文件路径(相对 cwd 或绝对)
384645
- * @param cwd 会话工作区目录(可选)
384646
- * @returns 引用串(如 @src/index.ts 或 @"a b.ts")
384647
- */
384648
- function mentionOf(path, cwd) {
384649
- const raw = String(path);
384650
- let rel = raw;
384651
- if (cwd) {
384652
- const base = String(cwd).replace(/[\\/]+$/, "");
384653
- const up = raw.replace(/\\/g, "/");
384654
- const baseUp = base.replace(/\\/g, "/");
384655
- if (up.startsWith(baseUp + "/")) rel = up.slice(baseUp.length + 1);
384656
- else if (up === baseUp) rel = "";
384657
- }
384658
- rel = rel.replace(/\\/g, "/").replace(/^\.\//, "");
384659
- if (!rel) rel = raw.replace(/\\/g, "/");
384660
- if (/[\u0000-\u001f\u007f-\u009f"]/u.test(rel)) return "@" + raw;
384661
- return /\s/u.test(rel) ? `@"${rel}"` : `@${rel}`;
384662
- }
384663
- /** 引用标签:文件名 + 可选行区间(chip 内展示,不带 @ 前缀)。
384664
- * @author ddj 2026年08月25号 */
384665
- function labelOf(path, range) {
384666
- const base = String(path).split(/[\\/]/).pop() || String(path);
384667
- if (!range || range.endLine < range.startLine) return base;
384668
- return range.startLine === range.endLine ? `${base} L${range.startLine}` : `${base} L${range.startLine}-${range.endLine}`;
384669
- }
384670
- /** 引用 ref:引用串 + 可选行区间(序列化直出,agent 据此读取对应片段)。
384671
- * @author ddj 2026年08月25号 */
384672
- function refOf(mention, range) {
384673
- if (!range || range.endLine < range.startLine) return mention;
384674
- return range.startLine === range.endLine ? `${mention} L${range.startLine}` : `${mention} L${range.startLine}-${range.endLine}`;
384675
- }
384676
- /** 构造 DSH 文件引用插入载荷,模型文本、剪贴板投影与降级文本共用完整行区间。
384677
- * @public
384678
- * @author ddj 2026年10月28号
384679
- * @returns 引用载荷与含可选行区间的完整引用文本
384680
- * @param path 目标路径(相对 cwd 或绝对)
384681
- * @param cwd 会话工作区目录(可选)
384682
- * @param range 行区间(可选)
384683
- * @param appearance 引用外观:'file'(默认)| 'folder'
384684
- */
384685
- function buildFileRef(path, cwd, range, appearance = "file") {
384686
- const mention = refOf(mentionOf(path, cwd), range);
384687
- return {
384688
- mention,
384689
- reference: {
384690
- source: REF_SOURCE,
384691
- ref: mention,
384692
- label: labelOf(path, range),
384693
- appearance,
384694
- clipboardText: mention
384695
- }
384696
- };
384697
- }
384698
- /** @private @author ddj 2026年10月29号 @param receipt Raw selected-channel receipt. @returns Explicit admission evidence and the historical boolean. */
384699
- function taskReceipt(receipt) {
384700
- const ok = receipt?.ok;
384701
- if (ok === true) return {
384702
- status: "accepted",
384703
- legacyOk: true
384704
- };
384705
- if (ok === false) return {
384706
- status: "rejected",
384707
- legacyOk: false
384708
- };
384709
- return {
384710
- status: "unknown",
384711
- legacyOk: false
384712
- };
384713
- }
384714
- /** @private @author ddj 2026年10月29号 @param ctx Lifecycle owner. @param sessions Public Controller. @param id Fixed target. @param text Task body. @returns Admission evidence without switching transport. @sideEffects Retains and releases exactly one generation. */
384715
- async function sendOwned(ctx, sessions, id, text) {
384716
- const controller = new AbortController();
384717
- const reference = sessions.retain(id, {
384718
- source: "controllerOperation",
384719
- signal: controller.signal
384720
- });
384721
- let released = false;
384722
- let cleanupFailed = false;
384723
- /** @private @author ddj 2026年10月29号 Cancels and releases this exact generation once; remembers unload cleanup failures. */
384724
- function release() {
384725
- if (released) return;
384726
- released = true;
384727
- controller.abort();
384728
- try {
384729
- reference.release();
384730
- } catch (error) {
384731
- cleanupFailed = true;
384732
- throw error;
384733
- }
384734
- }
384735
- let remove;
384736
- let started = false;
384737
- let result = {
384738
- status: "unavailable",
384739
- legacyOk: false
384740
- };
384741
- try {
384742
- remove = ctx.effect?.(() => release, "vscode-mode: task session reference");
384743
- const binding = await reference.ready;
384744
- if (controller.signal.aborted) result = {
384745
- status: "cancelled",
384746
- legacyOk: false
384747
- };
384748
- else {
384749
- started = true;
384750
- result = taskReceipt(await binding.session.prompt([{
384751
- type: "text",
384752
- text
384753
- }], "queue", controller.signal));
384754
- }
384755
- } catch {
384756
- result = {
384757
- status: started ? "unknown" : "unavailable",
384758
- legacyOk: false
384759
- };
384760
- }
384761
- const cancelled = controller.signal.aborted;
384762
- try {
384763
- release();
384764
- } catch {
384765
- cleanupFailed = true;
384766
- }
384767
- try {
384768
- remove?.();
384769
- } catch {
384770
- cleanupFailed = true;
384771
- }
384772
- if (cleanupFailed) result = {
384773
- status: started ? "unknown" : "unavailable",
384774
- legacyOk: false
384775
- };
384776
- if (cancelled) result = {
384777
- status: "cancelled",
384778
- legacyOk: result.legacyOk
384779
- };
384780
- return result;
384781
- }
384782
- /** @private @author ddj 2026年10月29号 @param ctx Service owner. @param id Fixed target. @param text Task body. @returns One legacy-channel attempt; no cross-channel retry. */
384783
- async function sendLegacy(ctx, id, text) {
384784
- const input = inputFor(ctx, id);
384785
- if (typeof input?.setDraft === "function" && typeof input.submit === "function") {
384786
- let submitted = false;
384787
- try {
384788
- input.setDraft(text);
384789
- submitted = true;
384790
- input.submit();
384791
- return {
384792
- status: "submitted",
384793
- legacyOk: true
384794
- };
384795
- } catch {
384796
- return {
384797
- status: submitted ? "unknown" : "unavailable",
384798
- legacyOk: false
384799
- };
384800
- }
384801
- }
384802
- const remote = ctx.get("remote.session");
384803
- if (typeof remote?.prompt !== "function") return httpTaskResult(id, text);
384804
- try {
384805
- const payload = {
384806
- sessionId: id,
384807
- mode: "queue",
384808
- content: [{
384809
- type: "text",
384810
- text
384811
- }]
384812
- };
384813
- const receipt = await remote.prompt(slotArg(payload));
384814
- return taskReceipt(receipt?.result ?? receipt);
384815
- } catch {
384816
- return {
384817
- status: "unknown",
384818
- legacyOk: false
384819
- };
384820
- }
384821
- }
384822
- /** apiproxy wire 端点(POST /api/<method>;浏览器同源 fetch 自带 cookie 鉴权;实证 401=未鉴权而非 404)。 */
384823
- const SESSION_CREATE_PATH = "/api/session/create";
384824
- /** apiproxy wire 端点:发送任务(session/prompt;点击即发送方案)。 */
384825
- const SESSION_PROMPT_PATH = "/api/session/prompt";
384826
- /** mint RPC 请求 id(RpcId 品牌类型运行时即普通字符串)。 */
384827
- function mintRpcId() {
384828
- try {
384829
- return globalThis.crypto?.randomUUID?.() ?? "rpc-" + Date.now();
384830
- } catch {
384831
- return "rpc-" + Date.now();
384832
- }
384833
- }
384834
- /**
384835
- * wire 槽实参组装(gateway `invoke` 铁证:`request.args[parameter.wire]` 具名槽,
384836
- * `session/prompt` 的槽名 = "request",值 = RpcRequest `{rpcId, payload}`)。
384837
- * @author ddj 2026年09月24号
384838
- * @param payload 业务载荷
384839
- * @returns 具名槽实参 `{ args: { request: RpcRequest } }`
384840
- */
384841
- function slotArg(payload) {
384842
- return { args: { request: {
384843
- rpcId: mintRpcId(),
384844
- payload
384845
- } } };
384846
- }
384847
- /**
384848
- * RPC 返回解析(照官方 dsh-client-runtime 调用形态:`const { result } = await api.sessions.create(...)`)。
384849
- * 官方 api.sessions 面返回**信封** `{ rpcId, result: RpcResult }`——须解 .result;
384850
- * 兼容裸 RpcResult 形态(个别补丁面直接回 {ok, value})。
384851
- * @author ddj 2026年09月23号
384852
- * @param res 信封或裸 RpcResult
384853
- * @returns 归一后的 { ok, value }
384854
- */
384855
- function rpcOk(res) {
384856
- const r = res?.result ?? res;
384857
- return {
384858
- ok: r?.ok === true,
384859
- value: r?.value
384860
- };
384861
- }
384862
- /**
384863
- * wire 直调 session/create(主路径):ClientRequest 信封 → ServerResponse.result(RpcResult 形态)。
384864
- * 契约以当前安装版 dsh-host-apiproxy 为准(api/sessions.d.ts「Method signatures are the
384865
- * source of truth」):`create({cwd}) → {sessionId}`;信封 = `{type:'client-request', rpcId,
384866
- * method, payload}` → `{result: {ok:true, value:{sessionId}} | {ok:false, error}}`。
384867
- * @author ddj 2026年09月23号
384868
- * @param cwd 新会话工作区目录
384869
- * @returns 新会话 id(失败空串)
384870
- */
384871
- async function apiSessionCreate(cwd) {
384872
- const json = await (await fetch(appUrl(SESSION_CREATE_PATH), {
384873
- method: "POST",
384874
- headers: { "content-type": "application/json" },
384875
- body: JSON.stringify({
384876
- type: "client-request",
384877
- rpcId: mintRpcId(),
384878
- method: "session/create",
384879
- payload: cwd ? { cwd } : {}
384880
- })
384881
- })).json().catch(() => null);
384882
- const id = json?.result?.value?.sessionId;
384883
- return json?.result?.ok === true && typeof id === "string" ? id : "";
384884
- }
384885
- /** @private @author ddj 2026年10月29号 @param sessionId Fixed target. @param text Task body. @returns HTTP receipt evidence; errors after request entry are never safe rejection. */
384886
- async function httpTaskResult(sessionId, text) {
384887
- try {
384888
- const payload = {
384889
- sessionId,
384890
- mode: "queue",
384891
- content: [{
384892
- type: "text",
384893
- text
384894
- }]
384895
- };
384896
- return taskReceipt((await (await fetch(appUrl(SESSION_PROMPT_PATH), {
384897
- method: "POST",
384898
- headers: { "content-type": "application/json" },
384899
- body: JSON.stringify({
384900
- type: "client-request",
384901
- rpcId: mintRpcId(),
384902
- method: "session/prompt",
384903
- payload: slotArg(payload)
384904
- })
384905
- })).json().catch(() => null))?.result);
384906
- } catch {
384907
- return {
384908
- status: "unknown",
384909
- legacyOk: false
384910
- };
384911
- }
384912
- }
384913
- /**
384914
- * 新建独立会话承载旁路任务(02-deep-session-prompt:深度分析不污染当前对话)。
384915
- * 优先公开 sessions.create;旧 Host 按能力选择 remote 或 wire,调用后不换通道重试。
384916
- * 受理状态不明时返回 ok:false,不重复创建会话。
384917
- * @author ddj 2026年10月28号
384918
- * @author ddj 2026年09月23号
384919
- * @param ctx 客户端服务上下文(remote.session + sessions,仅回落用)
384920
- * @param cwd 新会话工作区目录(与当前会话同工作区)
384921
- * @returns 新会话结果(含 id)
384922
- */
384923
- async function newDraftSession(ctx, cwd) {
384924
- try {
384925
- const sessions = ctx.get("sessions");
384926
- if (typeof sessions?.create === "function") {
384927
- const created = await sessions.create(cwd ? { cwd } : void 0);
384928
- const id = typeof created === "string" && created ? created : typeof created?.id === "string" ? created.id : typeof created?.sessionId === "string" ? created.sessionId : typeof created?.session?.id === "string" ? created.session.id : "";
384929
- if (id) return {
384930
- ok: true,
384931
- id
384932
- };
384933
- }
384934
- if (typeof sessions?.create === "function") return { ok: false };
384935
- } catch {
384936
- return { ok: false };
384937
- }
384938
- try {
384939
- const remote = ctx.get("remote.session");
384940
- if (typeof remote?.create === "function") {
384941
- const payload = cwd ? { cwd } : {};
384942
- const receipt = rpcOk(await remote.create(slotArg(payload)));
384943
- const created = receipt.value;
384944
- if (receipt.ok && typeof created?.sessionId === "string") return {
384945
- ok: true,
384946
- id: created.sessionId
384947
- };
384948
- return { ok: false };
384949
- }
384950
- } catch {
384951
- return { ok: false };
384952
- }
384953
- try {
384954
- const id = await apiSessionCreate(cwd);
384955
- if (id) return {
384956
- ok: true,
384957
- id
384958
- };
384959
- } catch {}
384960
- return { ok: false };
384961
- }
384962
- /**
384963
- * 解析会话输入门面;缺失时返回 undefined(动作侧守卫降级)。
384964
- * @author ddj 2026年08月25号
384965
- * @param ctx 客户端服务上下文
384966
- * @param sessionId 会话 id
384967
- * @returns SessionInput 或 undefined
384968
- */
384969
- function inputFor(ctx, sessionId) {
384970
- if (!sessionId) return void 0;
384971
- try {
384972
- const sessions = ctx.get("sessions");
384973
- const conversation = ctx.get("conversation");
384974
- const actx = typeof sessions?.binding === "function" ? sessions.binding(sessionId)?.ctx : sessions?.scope?.(sessionId);
384975
- const shell = actx && conversation?.input?.for?.(actx);
384976
- return shell && typeof shell.insertReference === "function" ? shell : void 0;
384977
- } catch {
384978
- return;
384979
- }
384980
- }
384981
- /** 取当前草稿快照(长度/版本/chip 列表);无输入门面时返回 null。
384982
- * @author ddj 2026年08月25号 / 2026年09月11号 */
384983
- function draftCursor(input) {
384984
- try {
384985
- const s = input?.state?.getSnapshot?.();
384986
- if (!s) return null;
384987
- return {
384988
- draft: String(s.draft ?? ""),
384989
- draftRev: Number(s.draftRev ?? 0),
384990
- occurrences: Array.isArray(s.occurrences) ? s.occurrences : void 0
384991
- };
384992
- } catch {
384993
- return null;
384994
- }
384995
- }
384996
- /** 从 sessions 列表快照读会话工作区目录(与 index.ts 既有读取方式一致)。
384997
- * @author ddj 2026年08月25号 */
384998
- function cwdOf(ctx, sessionId) {
384999
- if (!sessionId) return void 0;
385000
- try {
385001
- return ctx.get("sessions")?.list?.getSnapshot?.()?.byId?.[sessionId]?.cwd;
385002
- } catch {
385003
- return;
385004
- }
385005
- }
385006
- /** 输入门面插入引用(拦截异常视作未应用,走降级)。
385007
- * @author ddj 2026年08月25号 */
385008
- function safeInsert(input, reference, span) {
385009
- try {
385010
- return input.insertReference(reference, span) === true;
385011
- } catch {
385012
- return false;
385013
- }
385014
- }
385015
- /** 输入门面插入纯文本(官方 plain-text 通道;不存在或异常视作未应用)。
385016
- * @author ddj 2026年09月11号 */
385017
- function safeInsertText(input, text, span) {
385018
- if (typeof input.insertText !== "function") return false;
385019
- try {
385020
- return input.insertText(text, span) === true;
385021
- } catch {
385022
- return false;
385023
- }
385024
- }
385025
- /**
385026
- * 读输入框当前光标(detect 坐标);门面缺失/异常返回 null(由调用方回落到文档末尾)。
385027
- * @author ddj 2026年09月11号
385028
- * @param input 输入门面
385029
- * @returns caretSpan 结果或 null
385030
- */
385031
- function caretOf(input) {
385032
- if (typeof input.caretSpan !== "function") return null;
385033
- try {
385034
- return input.caretSpan() ?? null;
385035
- } catch {
385036
- return null;
385037
- }
385038
- }
385039
- /**
385040
- * 创建「添加到对话」动作集(apply 阶段构建一次,随 props 传给 EditorView)。
385041
- *
385042
- * 插入位置 = 输入框**当前光标**(`caretSpan()`,detect 坐标;无光标提示时回落文档末尾),
385043
- * 不再固定追加到最末尾。坐标一律用 **detect 投影**(见 detectEndOf 的缺陷说明);
385044
- * 两条写入通道都失败时**不写任何内容**并返回 'failed' —— 旧实现在此处退回 `setDraft`
385045
- * 整篇重建,会把既有 chip 全部销毁(连续添加多个引用即触发)。
385046
- * @author ddj 2026年08月25号 / 2026年09月11号
385047
- * @param ctx 客户端服务上下文(sessions + conversation)
385048
- * @returns 动作集
385049
- */
385050
- function createAddToConversation(ctx) {
385051
- const appendReference = async (sessionId, path, range, appearance) => {
385052
- const input = inputFor(ctx, sessionId);
385053
- if (!input) return "unavailable";
385054
- const cur = draftCursor(input);
385055
- if (!cur) return "unavailable";
385056
- const { reference, mention } = buildFileRef(path, cwdOf(ctx, sessionId), range, appearance);
385057
- const at = insertOffsetOf(caretOf(input), detectEndOf(cur));
385058
- const span = {
385059
- start: at,
385060
- end: at,
385061
- draftRev: cur.draftRev
385062
- };
385063
- if (safeInsert(input, reference, span)) return "ok";
385064
- const pad = paddingAt(cur, at);
385065
- if (safeInsertText(input, (pad.lead ? " " : "") + mention + (pad.tail ? " " : ""), span)) return "busy";
385066
- return "failed";
385067
- };
385068
- /**
385069
- * 追加纯文本(AI 深度分析任务注入):走官方 plain-text 通道插到当前光标。
385070
- * 失败语义与 appendReference 一致:宁可不写也不整篇重建(保护既有 chip)。
385071
- * @author ddj 2026年09月23号
385072
- * @param sessionId 会话 id
385073
- * @param text 待注入文本
385074
- * @returns 注入结果(ok / unavailable / failed)
385075
- */
385076
- const appendText = async (sessionId, text) => {
385077
- const input = inputFor(ctx, sessionId);
385078
- if (!input) return "unavailable";
385079
- const cur = draftCursor(input);
385080
- if (!cur) return "unavailable";
385081
- const at = insertOffsetOf(caretOf(input), detectEndOf(cur));
385082
- return safeInsertText(input, text, {
385083
- start: at,
385084
- end: at,
385085
- draftRev: cur.draftRev
385086
- }) ? "ok" : "failed";
385087
- };
385088
- /**
385089
- * 新建独立会话(02-deep-session-prompt:旁路任务不污染当前对话)。
385090
- * 闭包持有真服务 ctx(面板 SidebarCtx 无 .get,会话操作必须经此门面);
385091
- * 新版仅返回已登记身份,视图选择由宿主负责;旧版保留可用的 open/select。
385092
- * @author ddj 2026年10月28号
385093
- * @author ddj 2026年09月23号
385094
- * @param cwd 新会话工作区目录(与当前会话同工作区)
385095
- * @returns 新会话结果(失败供降级)
385096
- */
385097
- const startDraftSession = async (cwd) => {
385098
- const result = await newDraftSession(ctx, cwd);
385099
- if (typeof ctx.get("sessions")?.retain === "function") return result;
385100
- if (result.ok && result.id) try {
385101
- const sessions = ctx.get("sessions");
385102
- sessions?.materializeScope?.(result.id);
385103
- const title = "AI 深度分析 · " + (String(cwd ?? "").split(/[\\/]/).pop() || "SVN 变更");
385104
- sessions?.manager?.rename?.(result.id, title);
385105
- sessions?.manager?.select?.(result.id);
385106
- sessions?.open?.(result.id);
385107
- } catch {}
385108
- return result;
385109
- };
385110
- /**
385111
- * 发送任务给目标会话 agent(点击即发送方案:任务落地会话即非 blank,列表可见)。
385112
- * 新版持有目标 Controller 到 prompt 结束;旧版按能力选择唯一通道,不跨通道重发。
385113
- * @author ddj 2026年10月29号
385114
- * @param sessionId 目标会话 id
385115
- * @param text 任务文本
385116
- * @returns 同一次调用的历史boolean语义,不能作为受理事实
385117
- */
385118
- const sendTask = async (sessionId, text) => (await sendTaskResult(sessionId, text)).legacyOk;
385119
- /** @private @author ddj 2026年10月29号 @param sessionId Fixed target. @param text Task body. @returns Detailed evidence from exactly one selected transport. */
385120
- async function sendTaskResult(sessionId, text) {
385121
- if (!sessionId) return {
385122
- status: "unavailable",
385123
- legacyOk: false
385124
- };
385125
- try {
385126
- const sessions = ctx.get("sessions");
385127
- if (typeof sessions?.retain === "function") return await sendOwned(ctx, sessions, sessionId, text);
385128
- return await sendLegacy(ctx, sessionId, text);
385129
- } catch {
385130
- return {
385131
- status: "unavailable",
385132
- legacyOk: false
385133
- };
385134
- }
385135
- }
385136
- return {
385137
- appendReference,
385138
- appendText,
385139
- startDraftSession,
385140
- sendTask,
385141
- sendTaskResult
385142
- };
385143
- }
385144
385886
  /**
385145
385887
  * 由选区文本推导搜索种子。
385146
385888
  * @author ddj 2026年09月18号
@@ -385450,6 +386192,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
385450
386192
  at: 0
385451
386193
  };
385452
386194
  let progressListeners = /* @__PURE__ */ new Set();
386195
+ let statusRequest = null;
386196
+ let statusEpoch = 0;
385453
386197
  /** 会话 id 变更通知(EditorView 持有权威 sessionId,经窗口事件回填)。 */
385454
386198
  const SESSION_EVENT = "edrv:lsp-session";
385455
386199
  /** 上报可见状态(复用 EditorView 的状态通道;失败不再静默)。 */
@@ -385469,6 +386213,11 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
385469
386213
  syncTimers.clear();
385470
386214
  docVersions.clear();
385471
386215
  sessionId$1 = id;
386216
+ statusEpoch++;
386217
+ statusCache = {
386218
+ servers: [],
386219
+ at: 0
386220
+ };
385472
386221
  refreshStatus();
385473
386222
  }
385474
386223
  /**
@@ -385728,17 +386477,35 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
385728
386477
  if (!res || !res.tokens || !Array.isArray(res.tokens.data)) return null;
385729
386478
  return res.tokens;
385730
386479
  }
385731
- /** 拉取服务器状态(带 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. */
385732
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;
385733
386489
  if (!force && Date.now() - statusCache.at < 1500) return statusCache.servers;
385734
- const res = await rpc("edrv.lsp.status", { sessionId: sessionId$1 }).catch(() => null);
385735
- const servers = res && res.ok && Array.isArray(res.servers) ? res.servers : [];
385736
- statusCache = {
385737
- servers,
385738
- 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)
385739
386494
  };
385740
- publishProgress();
385741
- 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
+ }
385742
386509
  }
385743
386510
  /** file:// 或已转 edrv:// 的 LSP location → Monaco Location(edrv:// uri + 1-based range)。 */
385744
386511
  function toClientLocation(loc) {
@@ -388811,6 +389578,13 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
388811
389578
  label: "添加到对话",
388812
389579
  disabled: !(hasSession && canAddToConversation)
388813
389580
  },
389581
+ ...state.forgeReady ? [{
389582
+ id: "forge-open",
389583
+ label: "打开 Forge"
389584
+ }, {
389585
+ id: "forge-local",
389586
+ label: "查看本地变更(Forge)"
389587
+ }] : [],
388814
389588
  {
388815
389589
  id: "close",
388816
389590
  label: "关闭",
@@ -393737,9 +394511,11 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
393737
394511
  page: null,
393738
394512
  error: ""
393739
394513
  });
393740
- if (!sessionId) return void 0;
393741
- return subscribeReview(sessionId, setReview);
393742
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]);
393743
394519
  const [tabs, setTabs] = react$1.default.useState([]);
393744
394520
  const [active, setDiskActive] = react$1.default.useState(null);
393745
394521
  const [planTabs, setPlanTabs] = react$1.default.useState([]);
@@ -394041,10 +394817,11 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
394041
394817
  const layoutRef = react$1.default.useRef(layout);
394042
394818
  layoutRef.current = layout;
394043
394819
  if (!diffRendererRef.current) diffRendererRef.current = createDiffRenderer((sid, t) => dbg(sid, t));
394820
+ const coveredPaths = react$1.default.useMemo(() => coveredPathKeys(review.page), [review.page]);
394044
394821
  const currentRecords = react$1.default.useMemo(() => {
394045
394822
  const list = [];
394046
394823
  for (const rec of Object.values(records)) {
394047
- if (rec.archived === true || officialPath(rec.path, review.page)) continue;
394824
+ if (rec.archived === true || coveredPaths.has(diffPathKey(rec.path))) continue;
394048
394825
  if (sameFile(rec.path, active)) list.push(rec);
394049
394826
  }
394050
394827
  return list;
@@ -394052,7 +394829,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
394052
394829
  records,
394053
394830
  active,
394054
394831
  cwd,
394055
- review.page
394832
+ coveredPaths
394056
394833
  ]);
394057
394834
  const reviewOpsRef = react$1.default.useRef(/* @__PURE__ */ new Map());
394058
394835
  const settledRef = react$1.default.useRef(/* @__PURE__ */ new Set());
@@ -395047,7 +395824,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
395047
395824
  });
395048
395825
  };
395049
395826
  react$1.default.useEffect(() => {
395050
- if (!sessionId) return void 0;
395827
+ if (!sessionId || props.visible === false) return void 0;
395051
395828
  const offRecords = subscribeRecords(sessionId, applyRecords);
395052
395829
  const offErrors = subscribeErrors(sessionId, (message) => setError("list异常:" + message));
395053
395830
  refreshRecords();
@@ -395055,7 +395832,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
395055
395832
  offRecords();
395056
395833
  offErrors();
395057
395834
  };
395058
- }, [sessionId]);
395835
+ }, [sessionId, props.visible]);
395059
395836
  /**
395060
395837
  * 展示外部同步提示(按路径+类型去重:同一文件重复回调不重置已关闭的提示)。
395061
395838
  * @author ddj 2026年09月15号
@@ -395174,17 +395951,31 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
395174
395951
  onDiskChange: (change) => diskChangeRef.current?.(change)
395175
395952
  });
395176
395953
  react$1.default.useEffect(() => {
395177
- 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
+ };
395178
395968
  const unsubscribe = onLspProgress(publishLsp);
395179
- refreshStatus(true).then(publishLsp);
395180
- const timer = setInterval(() => {
395181
- refreshStatus(true).then(publishLsp);
395182
- }, 1500);
395969
+ refreshLsp();
395970
+ const timer = setInterval(refreshLsp, 1500);
395971
+ document.addEventListener("visibilitychange", refreshLsp);
395183
395972
  return () => {
395973
+ alive = false;
395184
395974
  unsubscribe();
395185
395975
  clearInterval(timer);
395976
+ document.removeEventListener("visibilitychange", refreshLsp);
395186
395977
  };
395187
- }, [sessionId]);
395978
+ }, [sessionId, props.visible]);
395188
395979
  react$1.default.useEffect(() => {
395189
395980
  if (!sessionId) return;
395190
395981
  setSession(sessionId);
@@ -395234,6 +396025,9 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
395234
396025
  const fileOpenRef = react$1.default.useRef(null);
395235
396026
  /** @private @author ddj 2026年10月08号 @param e Routed file request. Always uses the current workspace and save/preview callbacks. */
395236
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;
395237
396031
  const p = e?.detail?.path;
395238
396032
  if (!p) return;
395239
396033
  const normalized = tabPath(p);
@@ -397043,11 +397837,13 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
397043
397837
  dbg(sessionId, `[DEBUG diffAction] stage=reply trace=${traceId} method=decideBatch items=${items.length} rpcMs=${(performance.now() - start).toFixed(1)} ok=${res?.ok === true}`);
397044
397838
  } catch (e) {}
397045
397839
  if (!res || !res.ok || !Array.isArray(res.results)) {
397046
- setError(res?.error ? String(res.error) : "批量处理失败");
397840
+ const failure = res?.error ? String(res.error) : "批量处理失败";
397841
+ setError(failure);
397047
397842
  return {
397048
397843
  ok: 0,
397049
397844
  fail: items.length,
397050
- stale: 0
397845
+ stale: 0,
397846
+ error: failure
397051
397847
  };
397052
397848
  }
397053
397849
  let ok = 0;
@@ -397066,11 +397862,13 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
397066
397862
  stale
397067
397863
  };
397068
397864
  }).catch((e) => {
397069
- setError("批量处理异常:" + String(e));
397865
+ const failure = "批量处理异常:" + String(e);
397866
+ setError(failure);
397070
397867
  return {
397071
397868
  ok: 0,
397072
397869
  fail: items.length,
397073
- stale: 0
397870
+ stale: 0,
397871
+ error: failure
397074
397872
  };
397075
397873
  });
397076
397874
  };
@@ -397239,6 +398037,27 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
397239
398037
  if (lifetime?.live) setReviewBatchBusy(false);
397240
398038
  }
397241
398039
  }
398040
+ /**
398041
+ * 批量失败提示:Host 原文优先,只有拿不到原因时才按计数兜底。
398042
+ * @author ddj 2026年11月3号
398043
+ * @param result actMany 的结果
398044
+ * @returns 提示文案;没有失败返回 null
398045
+ */
398046
+ const batchError = (result) => {
398047
+ if (!result.error) return batchFailText(result);
398048
+ if (isSessionMissingError(result.error)) return "这个会话没在当前端激活,Keep/Undo 没有执行(记录未改动);在会话里发一条消息激活后重试";
398049
+ return result.error;
398050
+ };
398051
+ /**
398052
+ * 批量结果没有 Host 原文时的计数兜底文案。
398053
+ * @author ddj 2026年11月3号
398054
+ * @param result actMany 的结果
398055
+ * @returns 提示文案;没有失败返回 null
398056
+ */
398057
+ const batchFailText = (result) => {
398058
+ if (!result.fail) return null;
398059
+ return result.stale ? `${result.fail} 处未处理(其中 ${result.stale} 处已不在文件中,只更新了记录),可刷新后重试` : `${result.fail} 处未处理,可刷新后重试`;
398060
+ };
397242
398061
  const acceptFile = () => {
397243
398062
  if (missingNow && !officialDetail) return decideMissing(false);
397244
398063
  if (officialDetail) return decideOfficial(false);
@@ -397252,8 +398071,9 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
397252
398071
  paths: active ? [active] : void 0,
397253
398072
  kind: "structure"
397254
398073
  });
397255
- setStatus(decideStatus("已采纳 ", result));
397256
- if (result.fail) setError(result.fail + " 处差异处理失败(可能已被后续修改影响),可刷新后重试");
398074
+ setStatus(result.ok ? `${SIDECAR_KEEP_DONE}(${result.ok} 处)` : decideStatus("已采纳 ", result));
398075
+ const message = batchError(result);
398076
+ if (message) setError(message);
397257
398077
  }).catch(() => {
397258
398078
  batchBusyRef.current = false;
397259
398079
  });
@@ -397272,7 +398092,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
397272
398092
  kind: "structure"
397273
398093
  });
397274
398094
  setStatus(decideStatus("已不采纳 ", result));
397275
- if (result.fail) setError(result.fail + " 处差异处理失败(可能已被后续修改影响),可刷新后重试");
398095
+ const message = batchError(result);
398096
+ if (message) setError(message);
397276
398097
  }).catch(() => {
397277
398098
  batchBusyRef.current = false;
397278
398099
  });
@@ -397280,7 +398101,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
397280
398101
  const allPending = react$1.default.useMemo(() => {
397281
398102
  const out = [];
397282
398103
  for (const rec of Object.values(records)) {
397283
- if (rec.superseded === true || officialPath(rec.path, review.page)) continue;
398104
+ if (rec.superseded === true || coveredPaths.has(diffPathKey(rec.path))) continue;
397284
398105
  const perHunk = Array.isArray(rec.decisions?.perHunk) ? rec.decisions.perHunk : [];
397285
398106
  const hunks = Array.isArray(rec.hunks) ? rec.hunks : [];
397286
398107
  for (let i = 0; i < hunks.length; i++) if ((perHunk.length ? perHunk[i] : rec.decisions.call) === "pending" && !noopHunk(rec, hunks[i])) out.push({
@@ -397369,7 +398190,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
397369
398190
  kind: "structure"
397370
398191
  });
397371
398192
  setStatus(decideStatus(reject ? "已不采纳 " : "已采纳 ", result) + `;删除文件 ${handled.size} 个`);
397372
- if (result.fail) setError(result.fail + " 处差异处理失败,可刷新后重试");
398193
+ const message = batchError(result);
398194
+ if (message) setError(message);
397373
398195
  } catch (cause) {
397374
398196
  if (lifetime?.live) setError(String(cause));
397375
398197
  } finally {
@@ -397414,6 +398236,88 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
397414
398236
  setStatus(`官方待审列表正在载入,载入完成后请再次点击${action}`);
397415
398237
  return false;
397416
398238
  }
398239
+ /** @private @author ddj 2026年11月03号 @description 批量通道的一次性完整快照(分页口径的 pending 只是页内计数)。 @returns 完整待审页 */
398240
+ async function batchSnapshot() {
398241
+ const res = await rpc("edrv.review.list", {
398242
+ sessionId,
398243
+ all: true
398244
+ });
398245
+ if (!res.ok) throw new Error(res.error);
398246
+ return res.page;
398247
+ }
398248
+ /** @private @author ddj 2026年11月03号 @param token 起批时的会话寿命 @returns 逐条 Undo 的精确部分完成(冲突处停止) */
398249
+ function undoAllBatch(token) {
398250
+ return runReviewBatch({
398251
+ snapshot: batchSnapshot,
398252
+ detail: async (id) => {
398253
+ const res = await rpc("edrv.review.detail", {
398254
+ sessionId,
398255
+ id
398256
+ });
398257
+ if (!res.ok) throw new Error(res.error);
398258
+ return res.detail;
398259
+ },
398260
+ decide: (detail, undo, operationId) => rpc("edrv.review.decide", {
398261
+ sessionId,
398262
+ id: detail.item.id,
398263
+ seq: detail.item.seq,
398264
+ revision: detail.item.revision,
398265
+ operationId,
398266
+ scope: "file",
398267
+ decision: undo ? "rejected" : "accepted"
398268
+ }),
398269
+ guard: () => guardReviewAll(true, token),
398270
+ operation: batchOperation
398271
+ }, true);
398272
+ }
398273
+ /** @private @author ddj 2026年11月03号 @param token 起批时的会话寿命 @returns 一次 RPC 的批量 Keep 结果(跳过项不算失败) */
398274
+ function keepAllBatch(token) {
398275
+ return runKeepAll({
398276
+ snapshot: batchSnapshot,
398277
+ keepAll: (items, batchId) => rpc("edrv.review.keepAll", {
398278
+ sessionId,
398279
+ batchId,
398280
+ items
398281
+ }),
398282
+ guard: () => guardReviewAll(false, token)
398283
+ });
398284
+ }
398285
+ /**
398286
+ * @private
398287
+ * @author ddj 2026年11月03号
398288
+ * @description: 批量结果落界面:刷新列表、状态栏计数;Keep 的跳过只做提示不当错误,
398289
+ * Undo 保持「遇冲突停止」口径。
398290
+ * @param result 批量结果
398291
+ * @param reject 本次是 Undo 还是 Keep
398292
+ * @param undoPath 起批时的活动文件
398293
+ * @param undoModel 起批时的编辑器模型
398294
+ * @param undoVersion 起批时的模型版本
398295
+ * @returns 官方段是否完全成功(legacy 通道是否继续)
398296
+ */
398297
+ function finishReviewAll(result, reject, undoPath, undoModel, undoVersion) {
398298
+ refreshReview(sessionId);
398299
+ if (result.paths.length) emitRefresh({
398300
+ scope,
398301
+ paths: result.paths,
398302
+ kind: "structure"
398303
+ });
398304
+ if (reject) {
398305
+ if (result.paths.some((path) => sameFile(path, undoPath)) && activeRef.current === undoPath && !dirtyRef.current[undoPath] && !savingRef.current.has(undoPath) && undoModel?.getVersionId?.() === undoVersion) reloadFile(true);
398306
+ setStatus(`Undo All:官方记录已完成 ${result.completed} / ${result.total}`);
398307
+ if (result.error) setError(`批量已停止,已完成 ${result.completed} 项,其余未处理。${result.error}`);
398308
+ return !result.error;
398309
+ }
398310
+ const skipped = result.skipped ?? [];
398311
+ setStatus(`Keep All:已保留 ${result.completed} / ${result.total}` + (skipped.length ? `,跳过 ${skipped.length} 条` : ""));
398312
+ if (result.error) setError(`Keep All 未全部完成:${result.error}`);
398313
+ if (skipped.length) toastDom(skippedTip(skipped));
398314
+ return !result.error;
398315
+ }
398316
+ /** @private @author ddj 2026年11月03号 @param skipped 被跳过的条目 @returns 最多 3 条、余量汇总的提示文案 */
398317
+ function skippedTip(skipped) {
398318
+ const head = skipped.slice(0, 3).map((item) => `${item.display}:${item.reason}`).join(";");
398319
+ return skipped.length > 3 ? `${head};等 ${skipped.length} 条` : head;
398320
+ }
397417
398321
  /** @private @author ddj 2026年10月27号 @param reject Undo or Keep all workspace pending records, including previous rounds. @returns Explicit partial results; legacy proceeds only when official stage fully succeeds. */
397418
398322
  async function actReviewAll(reject) {
397419
398323
  if (batchBusyRef.current) return;
@@ -397427,48 +398331,9 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
397427
398331
  setReviewBatchBusy(true);
397428
398332
  setStatus(reject ? "正在检查并 Undo 全部待审记录…" : "正在 Keep 全部待审记录…");
397429
398333
  try {
397430
- const result = await runReviewBatch({
397431
- snapshot: async () => {
397432
- const res = await rpc("edrv.review.list", {
397433
- sessionId,
397434
- all: true
397435
- });
397436
- if (!res.ok) throw new Error(res.error);
397437
- return res.page;
397438
- },
397439
- detail: async (id) => {
397440
- const res = await rpc("edrv.review.detail", {
397441
- sessionId,
397442
- id
397443
- });
397444
- if (!res.ok) throw new Error(res.error);
397445
- return res.detail;
397446
- },
397447
- decide: (detail, undo, operationId) => rpc("edrv.review.decide", {
397448
- sessionId,
397449
- id: detail.item.id,
397450
- seq: detail.item.seq,
397451
- revision: detail.item.revision,
397452
- operationId,
397453
- scope: "file",
397454
- decision: undo ? "rejected" : "accepted"
397455
- }),
397456
- guard: () => guardReviewAll(reject, token),
397457
- operation: batchOperation
397458
- }, reject);
398334
+ const result = reject ? await undoAllBatch(token) : await keepAllBatch(token);
397459
398335
  if (!token?.live) return;
397460
- refreshReview(sessionId);
397461
- if (reject && result.paths.some((path) => sameFile(path, undoPath)) && activeRef.current === undoPath && !dirtyRef.current[undoPath] && !savingRef.current.has(undoPath) && undoModel?.getVersionId?.() === undoVersion) reloadFile(true);
397462
- if (result.paths.length) emitRefresh({
397463
- scope,
397464
- paths: result.paths,
397465
- kind: "structure"
397466
- });
397467
- setStatus(`${reject ? "Undo All" : "Keep All"}:官方记录已完成 ${result.completed} / ${result.total}`);
397468
- if (result.error) {
397469
- setError(`批量已停止,已完成 ${result.completed} 项,其余未处理。${result.error}`);
397470
- return;
397471
- }
398336
+ if (!finishReviewAll(result, reject, undoPath, undoModel, undoVersion)) return;
397472
398337
  guardReviewAll(reject, token);
397473
398338
  if (allPending.length) {
397474
398339
  batchBusyRef.current = false;
@@ -397926,6 +398791,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
397926
398791
  }
397927
398792
  const tabMenuActions = (path) => ({
397928
398793
  "add-to-conversation": () => addRefToChat(path),
398794
+ "forge-open": () => props.forge?.open(sessionId),
398795
+ "forge-local": () => props.forge?.open(sessionId, true),
397929
398796
  "split-left": () => splitTab(path, "left"),
397930
398797
  "split-right": () => splitTab(path, "right"),
397931
398798
  "split-top": () => splitTab(path, "top"),
@@ -397972,6 +398839,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
397972
398839
  cwd,
397973
398840
  hasSession: Boolean(sessionId),
397974
398841
  canAddToConversation: Boolean(addToConversation),
398842
+ forgeReady: props.forge?.available() === true,
397975
398843
  canSplit: workbench.groups.length < 8 && !workbench.floats.some((item) => item.group === ownerOf(workbench, tabMenu.path)?.id),
397976
398844
  canFloat: workbench.groups.length < 8,
397977
398845
  canReturn: ownerOf(workbench, tabMenu.path)?.id !== MAIN_GROUP,
@@ -398354,6 +399222,48 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
398354
399222
  y: event.clientY
398355
399223
  });
398356
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
+ ]);
398357
399267
  /** @private @author ddj 2026年10月14号 @param group Auxiliary group. Return documents rather than closing them. */
398358
399268
  function returnPane(group) {
398359
399269
  acceptLayout(returnGroup(benchRef.current, group));
@@ -399133,6 +400043,9 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
399133
400043
  editor: () => editorRef.current,
399134
400044
  outlineSources: props.outlineSources,
399135
400045
  fileMenuItems: props.fileMenuItems,
400046
+ officialMenus: props.officialMenus,
400047
+ visible: props.visible,
400048
+ forge: props.forge,
399136
400049
  addToConversation,
399137
400050
  svn: svnStatus,
399138
400051
  svnChanges,
@@ -403189,6 +404102,144 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
403189
404102
  return new McpSessionState(fetchStatus, document);
403190
404103
  }
403191
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
403192
404243
  //#region src/client/ui/McpSessionStatus.ts
403193
404244
  /** Official session-row slots; no DOM patching or session activation. @author ddj 2026年10月20号 */
403194
404245
  const labels = {
@@ -403456,577 +404507,6 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
403456
404507
  }
403457
404508
  }
403458
404509
  //#endregion
403459
- //#region src/shared/externalOpen.ts
403460
- /**
403461
- * dsh-vscode-mode shared — 外部深链 URL 契约(Windows launcher / Unity 包 / client 共用)。
403462
- * 形态:?<FLAG>=1&<PATHS_KEY>=<enc1>[,<enc2>…] [&<LINE_KEY>=N] [&<COLUMN_KEY>=M]
403463
- * 每个路径独立 percent-encode(encodeURIComponent / Uri.EscapeDataString)后以 `,` 连接:
403464
- * 编码结果不含裸逗号(Windows 路径本身不允许逗号,编码兜底其余字符),URLSearchParams
403465
- * 取值做一次标准解码即得 `enc1,enc2`,按 `,` split 即路径,无需二次 decode。
403466
- * Purity rule: no node/react imports(URLSearchParams 为标准全局)。
403467
- * 作者 ddj 2026-09-07
403468
- */
403469
- /** 深链开关参数名(值恒为 '1')。 */
403470
- const EDRV_OPEN_FLAG = "edrvOpen";
403471
- /** 路径列表参数名(逗号分隔的已编码路径)。 */
403472
- const EDRV_PATHS_KEY = "edrvPaths";
403473
- /** 目标行参数名(1-based,可选,作用于首个路径)。 */
403474
- const EDRV_LINE_KEY = "edrvLine";
403475
- /** 目标列参数名(1-based,可选,作用于首个路径)。 */
403476
- const EDRV_COLUMN_KEY = "edrvColumn";
403477
- /** 深链参数键全集(URL 清理用)。 */
403478
- const EDRV_PARAM_KEYS = [
403479
- EDRV_OPEN_FLAG,
403480
- EDRV_PATHS_KEY,
403481
- EDRV_LINE_KEY,
403482
- EDRV_COLUMN_KEY
403483
- ];
403484
- /**
403485
- * 解析深链查询串:非深链/路径为空 → null;行列仅接受正整数,非法忽略。
403486
- * @author ddj 2026年09月07号
403487
- * @param search location.search(含或不含前导 ? 均可)
403488
- * @returns 深链参数或 null
403489
- */
403490
- function parseOpenParams(search) {
403491
- const params = new URLSearchParams(search ?? "");
403492
- if (params.get("edrvOpen") !== "1") return null;
403493
- const paths = (params.get("edrvPaths") ?? "").split(",").map((item) => item.trim()).filter(Boolean);
403494
- if (!paths.length) return null;
403495
- return {
403496
- paths,
403497
- line: positiveIntOf(params.get(EDRV_LINE_KEY)),
403498
- column: positiveIntOf(params.get(EDRV_COLUMN_KEY))
403499
- };
403500
- }
403501
- /**
403502
- * 正整数解析(非法/非正 → undefined)。
403503
- * @author ddj 2026年09月07号
403504
- * @param raw 参数原文(可空)
403505
- * @returns 正整数或 undefined
403506
- */
403507
- function positiveIntOf(raw) {
403508
- const value = Number(raw);
403509
- return Number.isInteger(value) && value > 0 ? value : void 0;
403510
- }
403511
- //#endregion
403512
- //#region src/client/chooseDialog.ts
403513
- /**
403514
- * dsh-vscode-mode client — 工作区选择弹窗(深链打开规则 1)。
403515
- * 复用与「添加 MCP」一致的 vsm-mcp-modal / vsm-mcp-dialog 模态(页面居中卡片 + 遮罩 + 主题
403516
- * token),React portal 挂 body;Esc / 遮罩点击 / 取消 → null(中止流程不改任何状态)。
403517
- * 作者 ddj 2026-09-07
403518
- */
403519
- /** 选择弹窗(vsm-mcp-modal 遮罩 + vsm-mcp-dialog 卡片,Esc/遮罩点击取消)。 */
403520
- function ChoiceModal({ title, folder, done }) {
403521
- react$1.default.useEffect(() => {
403522
- const onKey = (event) => {
403523
- if (event.key === "Escape") done(null);
403524
- };
403525
- window.addEventListener("keydown", onKey);
403526
- return () => window.removeEventListener("keydown", onKey);
403527
- }, []);
403528
- return react$1.default.createElement("div", {
403529
- className: "vsm-mcp-modal",
403530
- onMouseDown: (event) => {
403531
- if (event.target === event.currentTarget) done(null);
403532
- }
403533
- }, react$1.default.createElement("div", { className: "vsm-mcp-dialog" }, react$1.default.createElement("h3", null, "在 DSH 中打开"), react$1.default.createElement("label", null, "文件夹 " + folder + " 位于已注册的工作区「" + title + "」内,选择打开方式:"), react$1.default.createElement("div", { className: "vsm-mcp-dialog-actions" }, react$1.default.createElement("button", { onClick: () => done(null) }, "取消"), react$1.default.createElement("button", { onClick: () => done("create") }, "新建工作区"), react$1.default.createElement("button", {
403534
- className: "vsm-primary",
403535
- onClick: () => done("recent")
403536
- }, "使用最近的工作区"))));
403537
- }
403538
- /**
403539
- * 弹出工作区选择弹窗(深链打开规则 1)。
403540
- * @author ddj 2026年09月07号
403541
- * @param title 匹配到的工作区标题
403542
- * @param folder 文件夹路径
403543
- * @returns 用户选择;null = 取消/中止
403544
- */
403545
- function chooseWorkspace(title, folder) {
403546
- return new Promise((resolve) => {
403547
- const host = document.createElement("div");
403548
- document.body.appendChild(host);
403549
- const root = (0, react_dom_client.createRoot)(host);
403550
- const done = (choice) => {
403551
- root.unmount();
403552
- host.remove();
403553
- resolve(choice);
403554
- };
403555
- root.render(react$1.default.createElement(ChoiceModal, {
403556
- title,
403557
- folder,
403558
- done
403559
- }));
403560
- });
403561
- }
403562
- //#endregion
403563
- //#region src/client/openFlow.ts
403564
- /**
403565
- * dsh-vscode-mode client — 深链打开规则引擎(资源管理器右键 / Unity 外部编辑器落地)。
403566
- * 规则:文件夹 ⊂ 某已注册工作区 → 页面内弹窗二选一(使用最近的工作区 / 新建工作区);
403567
- * 文件夹不在任何工作区 → 直达最近对话所在工作区新增;文件 → 最近对话 + 编辑器展开
403568
- * (无任何会话 → 以文件父目录注册工作区引导创建)。首路径驱动规则,其余路径:文件进
403569
- * 编辑器、文件夹补引用。纯函数可单测;编排依赖经 OpenDeps 注入(mock 可测)。
403570
- * 作者 ddj 2026-09-07
403571
- */
403572
- /** 等列表/输入就绪的节奏。 */
403573
- const READY_TIMEOUT_MS = 15e3;
403574
- const READY_TICK_MS = 300;
403575
- const REF_RETRY_MAX = 20;
403576
- const REF_RETRY_MS = 300;
403577
- /**
403578
- * 路径规范化:`\→/`、小写、去尾分隔符(Windows 大小写不敏感前缀匹配用)。
403579
- * @author ddj 2026年09月07号
403580
- */
403581
- function normPath(p) {
403582
- return String(p ?? "").replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
403583
- }
403584
- /**
403585
- * 目录包含判定:child === root 或 child 位于 root 之下。
403586
- * @author ddj 2026年09月07号
403587
- */
403588
- function containsPath(root, child) {
403589
- const r = normPath(root);
403590
- const c = normPath(child);
403591
- return c === r || c.startsWith(r + "/");
403592
- }
403593
- /**
403594
- * 父目录(文件引导创建工作区用;根路径回退自身)。
403595
- * @author ddj 2026年09月07号
403596
- */
403597
- function dirnameOf(p) {
403598
- const norm = String(p ?? "").replace(/[\\/]+$/, "");
403599
- const cut = Math.max(norm.lastIndexOf("/"), norm.lastIndexOf("\\"));
403600
- return cut > 0 ? norm.slice(0, cut) : norm;
403601
- }
403602
- /**
403603
- * 最近一次对话:排除子代理行,updatedAt 最新优先。
403604
- * @author ddj 2026年09月07号
403605
- */
403606
- function recentSession(byId, ids) {
403607
- return (ids ?? Object.keys(byId)).map((id) => byId[id]).filter((row) => row && row.origin !== "subagent").sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0];
403608
- }
403609
- /**
403610
- * 文件夹命中的已注册工作区(前缀包含),updatedAt 新者优先。
403611
- * @author ddj 2026年09月07号
403612
- */
403613
- function matchWorkspaces(items, folder) {
403614
- return items.filter((ws) => containsPath(ws.path, folder));
403615
- }
403616
- /**
403617
- * 规则分派:目录+命中工作区 → choose;目录未命中 → folderNew(以该文件夹为根注册新工作区,
403618
- * 用户确认:一律取右键的文件夹本身);文件有会话 → fileRecent;文件无会话 → bootstrap;
403619
- * 路径缺失 → abort。
403620
- * @author ddj 2026年09月08号
403621
- */
403622
- function pickRule(kind, matched, hasSession) {
403623
- if (kind === "missing") return "abort";
403624
- if (kind === "directory") return matched ? "choose" : "folderNew";
403625
- return hasSession ? "fileRecent" : "bootstrap";
403626
- }
403627
- /**
403628
- * 深链打开编排入口:等列表就绪 → 判型 → 规则分派 → 编排 create/open/引用/编辑器。
403629
- * @author ddj 2026年09月07号
403630
- * @param ctx 客户端根上下文(sessions/workspaces 服务)
403631
- * @param params 深链参数(首路径驱动规则)
403632
- * @param overrides 依赖覆盖(测试注入)
403633
- */
403634
- async function openDeepLink(ctx, params, overrides = {}) {
403635
- const deps = realDeps(ctx, overrides);
403636
- if (!await waitListsReady(deps)) {
403637
- deps.notify("DSH 会话列表未就绪,请稍后重试");
403638
- return false;
403639
- }
403640
- const primary = params.paths[0];
403641
- const kind = await deps.stat(primary).catch(() => "missing");
403642
- const sess = deps.sessions();
403643
- const rule = pickRule(kind, matchWorkspaces(deps.workspaces().items, primary).length > 0, Boolean(recentSession(sess.byId, sess.ids)));
403644
- try {
403645
- if (rule === "abort") {
403646
- deps.notify("路径不存在:" + primary);
403647
- return false;
403648
- }
403649
- if (rule === "choose") await runChoose(deps, primary, params);
403650
- else if (rule === "folderNew") await runFolderNew(deps, primary, params);
403651
- else if (rule === "fileRecent") await runFileRecent(deps, primary, params);
403652
- else await runFileBootstrap(deps, primary, params);
403653
- return true;
403654
- } catch (error) {
403655
- deps.notify("打开失败:" + String(error?.message ?? error));
403656
- return false;
403657
- }
403658
- }
403659
- /** 等待会话与工作区列表就绪(上限 15s;超时按当前态判定)。 */
403660
- async function waitListsReady(deps) {
403661
- for (let waited = 0; waited < READY_TIMEOUT_MS; waited += READY_TICK_MS) {
403662
- if (deps.sessions().ready && deps.workspaces().ready) return true;
403663
- await sleep(deps, READY_TICK_MS);
403664
- }
403665
- return deps.sessions().ready && deps.workspaces().ready;
403666
- }
403667
- /** 规则 1:命中工作区 → 弹窗分派(取消即中止)。 */
403668
- async function runChoose(deps, folder, params) {
403669
- const best = bestMatch(deps, matchWorkspaces(deps.workspaces().items, folder));
403670
- const choice = await deps.choose(best?.title ?? "工作区", folder);
403671
- if (choice === null) throw new Error("用户取消打开");
403672
- if (choice === "create") return runFolderNew(deps, folder, params);
403673
- const sessionId = await spawnInWorkspace(deps, (best ?? deps.workspaces().items[0]).workspaceId, void 0);
403674
- await settleRef(deps, sessionId, folder, "folder");
403675
- await openChecked(deps, sessionId, null);
403676
- await openRest(deps, sessionId, params, 1);
403677
- }
403678
- /** 规则 3a:文件 → 所属工作区优先(有会话打开其最近会话;无会话新建 + 文件引用);无匹配 → 最近对话。 */
403679
- async function runFileRecent(deps, file, params) {
403680
- const ws = pickFileWorkspace(deps, file);
403681
- if (ws) {
403682
- const anchor = recentSessionIn(deps, ws);
403683
- if (anchor) {
403684
- deps.openSession(anchor.id);
403685
- await openChecked(deps, anchor.id, file, params.line, params.column);
403686
- await openRest(deps, anchor.id, params, 1);
403687
- return;
403688
- }
403689
- const sessionId = await spawnInWorkspace(deps, ws.workspaceId, void 0);
403690
- await settleRef(deps, sessionId, file, "file");
403691
- await openChecked(deps, sessionId, file, params.line, params.column);
403692
- await openRest(deps, sessionId, params, 1);
403693
- return;
403694
- }
403695
- const anchor = recentSession(deps.sessions().byId, deps.sessions().ids);
403696
- if (!anchor) return runFileBootstrap(deps, file, params);
403697
- deps.openSession(anchor.id);
403698
- await openChecked(deps, anchor.id, file, params.line, params.column);
403699
- await openRest(deps, anchor.id, params, 1);
403700
- }
403701
- /**
403702
- * 文件所属工作区:前缀命中的工作区里,优先取有会话且最近活跃的;都无会话取路径最具体的。
403703
- * @author ddj 2026年09月08号
403704
- */
403705
- function pickFileWorkspace(deps, file) {
403706
- const matches = matchWorkspaces(deps.workspaces().items, file);
403707
- if (!matches.length) return void 0;
403708
- const withSessions = matches.filter((ws) => (ws.sessionIds ?? []).some((id) => Boolean(deps.sessions().byId[id])));
403709
- if (!withSessions.length) return [...matches].sort((a, b) => normPath(b.path).length - normPath(a.path).length)[0];
403710
- return withSessions.sort((a, b) => latestSessionAt(deps, b) - latestSessionAt(deps, a))[0];
403711
- }
403712
- /** 工作区内会话的最近活跃时间(无成员 → 0)。 */
403713
- function latestSessionAt(deps, ws) {
403714
- const byId = deps.sessions().byId;
403715
- return (ws.sessionIds ?? []).reduce((max, id) => Math.max(max, byId[id]?.updatedAt ?? 0), 0);
403716
- }
403717
- /** 工作区内最近一次会话(排除子代理与不在列表中的陈旧行)。 */
403718
- function recentSessionIn(deps, ws) {
403719
- const byId = deps.sessions().byId;
403720
- return (ws.sessionIds ?? []).map((id) => byId[id]).filter((row) => row && row.origin !== "subagent").sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0];
403721
- }
403722
- /** 规则 3b:文件 + 无任何会话 → 父目录注册工作区 + 新对话 + 文件引用 + 编辑器打开。 */
403723
- async function runFileBootstrap(deps, file, params) {
403724
- const sessionId = await spawnInWorkspace(deps, (await deps.createWorkspace(dirnameOf(file))).workspaceId, void 0);
403725
- await settleRef(deps, sessionId, file, "file");
403726
- await openChecked(deps, sessionId, file, params.line, params.column);
403727
- await openRest(deps, sessionId, params, 1);
403728
- }
403729
- /** 规则 1.2:以文件夹注册新工作区 + 新对话 + 文件夹引用 + 编辑页。 */
403730
- async function runFolderNew(deps, folder, params) {
403731
- const sessionId = await spawnInWorkspace(deps, (await deps.createWorkspace(folder)).workspaceId, void 0);
403732
- await settleRef(deps, sessionId, folder, "folder");
403733
- await openChecked(deps, sessionId, null);
403734
- await openRest(deps, sessionId, params, 1);
403735
- }
403736
- /** 在指定工作区(或回退 cwd)新建对话并置为当前;返回新会话 id。 */
403737
- async function spawnInWorkspace(deps, workspaceId, fallbackCwd) {
403738
- const sessionId = await deps.createSession(workspaceId ? { workspaceId } : fallbackCwd ? { cwd: fallbackCwd } : {});
403739
- deps.openSession(sessionId);
403740
- return sessionId;
403741
- }
403742
- /** 命中工作区集中「最近」的一个(其成员会话 updatedAt 最新者优先)。 */
403743
- function bestMatch(deps, matches) {
403744
- const byId = deps.sessions().byId;
403745
- const score = (ws) => {
403746
- const sessionLatest = (ws.sessionIds ?? []).reduce((max, id) => Math.max(max, byId[id]?.updatedAt ?? 0), 0);
403747
- return Math.max(sessionLatest, Date.parse(ws.updatedAt ?? "") || 0);
403748
- };
403749
- return [...matches].sort((a, b) => score(b) - score(a))[0];
403750
- }
403751
- /** 引用插入轮询:输入门面就绪前每 300ms 重试(appendReference 忙态已降级视为成功)。 */
403752
- async function settleRef(deps, sessionId, path, appearance) {
403753
- for (let attempt = 0; attempt < REF_RETRY_MAX; attempt++) {
403754
- if (await deps.reference(sessionId, path, appearance)) return;
403755
- await sleep(deps, REF_RETRY_MS);
403756
- }
403757
- throw new Error("引用插入未完成");
403758
- }
403759
- /** 其余路径:文件进编辑器(步进),文件夹补文件夹引用。 */
403760
- async function openRest(deps, sessionId, params, from) {
403761
- const rest = params.paths.slice(from);
403762
- for (const path of rest) {
403763
- const kind = await deps.stat(path).catch(() => "missing");
403764
- if (kind === "file") await openChecked(deps, sessionId ?? void 0, path);
403765
- else if (kind === "directory" && sessionId) await settleRef(deps, sessionId, path, "folder");
403766
- else throw new Error("路径不存在:" + path);
403767
- }
403768
- }
403769
- /** 可注入 sleep。 */
403770
- function sleep(deps, ms) {
403771
- return new Promise((resolve) => deps.schedule(resolve, ms));
403772
- }
403773
- /** 真实依赖装配(全部来自 ctx 注入服务 + 既有 RPC/弹窗/编辑器入口)。 */
403774
- function realDeps(ctx, overrides = {}) {
403775
- const service = (name) => {
403776
- const get = ctx?.get;
403777
- return typeof get === "function" ? get.call(ctx, name) : void 0;
403778
- };
403779
- const add = createAddToConversation(ctx);
403780
- return {
403781
- stat: async (path) => {
403782
- const result = await rpc("edrv.externalStat", { path });
403783
- return result.ok ? result.kind : "missing";
403784
- },
403785
- workspaces: () => {
403786
- const snap = service("workspaces")?.list?.getSnapshot?.() ?? {};
403787
- return {
403788
- items: snap.items ?? [],
403789
- ready: snap.phase === "ready"
403790
- };
403791
- },
403792
- sessions: () => {
403793
- const snap = service("sessions")?.list?.getSnapshot?.() ?? {};
403794
- return {
403795
- ids: snap.ids ?? [],
403796
- byId: snap.byId ?? {},
403797
- ready: snap.phase !== "pending"
403798
- };
403799
- },
403800
- createSession: async (opts) => {
403801
- const sessions = service("sessions");
403802
- if (typeof sessions?.create === "function") try {
403803
- const created = await sessions.create(opts);
403804
- const id = typeof created === "string" && created ? created : typeof created?.id === "string" ? created.id : typeof created?.sessionId === "string" ? created.sessionId : "";
403805
- if (id) return id;
403806
- } catch {}
403807
- const draft = await newDraftSession(ctx, opts.cwd);
403808
- if (draft.ok && draft.id) return draft.id;
403809
- throw new Error("新建会话失败");
403810
- },
403811
- createWorkspace: async (path) => {
403812
- const ws = await service("workspaces").create({ path });
403813
- return {
403814
- workspaceId: ws.workspaceId,
403815
- title: ws.title
403816
- };
403817
- },
403818
- openSession: (id) => {
403819
- try {
403820
- const sessions = service("sessions");
403821
- sessions?.materializeScope?.(id);
403822
- sessions?.manager?.select?.(id);
403823
- sessions?.open?.(id);
403824
- } catch {}
403825
- },
403826
- reference: async (sessionId, path, appearance) => {
403827
- const outcome = await add.appendReference(sessionId, path, void 0, appearance);
403828
- return outcome === "ok" || outcome === "busy";
403829
- },
403830
- choose: (title, folder) => chooseWorkspace(title, folder),
403831
- openEditor: requestOpen,
403832
- schedule: (fn, ms) => setTimeout(fn, ms),
403833
- notify: (text) => toastDom(text),
403834
- ...overrides
403835
- };
403836
- }
403837
- /** @private @author ddj 2026年09月28号
403838
- * Await actual editor completion and propagate failure to the request result.
403839
- * @param deps Open dependencies.
403840
- * @param sessionId Target session.
403841
- * @param path Target file or editor mount.
403842
- * @param line Optional line.
403843
- * @param column Optional column.
403844
- */
403845
- async function openChecked(deps, sessionId, path, line, column) {
403846
- if (!await deps.openEditor(path, line, column, sessionId)) throw new Error("编辑器打开未完成:" + (path ?? "工作区"));
403847
- }
403848
- //#endregion
403849
- //#region src/client/externalOpen.ts
403850
- /**
403851
- * dsh-vscode-mode client — 外部深链落地(Windows 右键菜单 / Unity 外部编辑器)。
403852
- * 启动时解析 location.search 的 edrvOpen 深链:跨源 referrer 守卫防外部网页诱导;
403853
- * 处理完剥离 URL 参数防刷新重开;执行委托 openFlow 打开规则引擎(工作区/对话智能路由)。
403854
- * 另挂 3s 移交轮询(兼活跃心跳):launcher 投递的打开请求由已打开页面就地执行,避免重复开新页。
403855
- * 作者 ddj 2026-09-07 / 2026-09-08
403856
- */
403857
- /** toast 自隐时长。 */
403858
- const TOAST_MS = 5e3;
403859
- /** 深链移交轮询周期(兼活跃心跳)。 */
403860
- const HANDOFF_POLL_MS = 3e3;
403861
- /**
403862
- * 页面隐藏时的降频倍数(3s × 5 = 15s)。
403863
- * 必须仍短于 host 租约(src/externalHandoff.ts 的 LEASE_MS = 30s):心跳一旦超期,
403864
- * host 判定该页面已死,launcher 会重复开新页——比多几次 RPC 更糟。
403865
- */
403866
- const HIDDEN_POLL_EVERY = 5;
403867
- /**
403868
- * 装配外部深链:深链参数缺失时仅保持移交轮询(零额外开销)。
403869
- * @author ddj 2026年09月07号
403870
- * @param ctx 客户端根上下文(sessions/workspaces 服务)
403871
- * @param options 可注入选项
403872
- */
403873
- function setupExtOpen(ctx, options = {}) {
403874
- const dispose = startPresencePoll(ctx);
403875
- const params = parseOpenParams(options.search ?? (typeof location !== "undefined" ? location.search : ""));
403876
- if (!params) return dispose;
403877
- const referrer = options.referrer ?? (typeof document !== "undefined" ? document.referrer : "");
403878
- if (!referrerAllowed(referrer, options.origin ?? (typeof location !== "undefined" ? location.origin : ""))) {
403879
- log$2.warn("已忽略跨源深链请求(referrer=" + referrer + ")");
403880
- return dispose;
403881
- }
403882
- stripCurrentUrl();
403883
- openDeepLink(ctx, params).catch((error) => {
403884
- log$2.warn("深链打开失败:" + String(error));
403885
- toastDom("深链打开失败:" + String(error?.message ?? error));
403886
- });
403887
- return dispose;
403888
- }
403889
- /**
403890
- * 深链移交轮询:可见时每 3s 向 host 领取待打开请求(兼活跃心跳,launcher 据此决定是否开新页);
403891
- * 页面隐藏时降到 15s 一次——无人看页面时移交不必保持 3s 精度,但心跳不能停(超期会让 host 判页面已死)。
403892
- * 页面级单例(window 标记防 HMR 重复);领取到即执行打开规则。
403893
- * @author ddj 2026年09月08号 / 2026年10月15号
403894
- * @param ctx 客户端根上下文
403895
- */
403896
- function startPresencePoll(ctx) {
403897
- if (typeof window === "undefined") return () => {};
403898
- const marker = window;
403899
- if (typeof marker.__edrvExtPoll === "function") marker.__edrvExtPoll();
403900
- const generatedId = createRequestId();
403901
- if (!generatedId) {
403902
- log$2.warn("安全随机源不可用,已停用外部打开心跳");
403903
- return () => {};
403904
- }
403905
- const clientId = generatedId;
403906
- let disposed = false;
403907
- let polling = false;
403908
- let executing = false;
403909
- let ticks = 0;
403910
- /**
403911
- * 页面隐藏时按倍数跳轮;可见时每次都跑并清零计数。
403912
- * @author ddj 2026年10月15号
403913
- * @returns 是否跳过本轮
403914
- */
403915
- function hiddenSkip() {
403916
- if (typeof document === "undefined" || document.visibilityState !== "hidden") {
403917
- ticks = 0;
403918
- return false;
403919
- }
403920
- ticks += 1;
403921
- return ticks % HIDDEN_POLL_EVERY !== 0;
403922
- }
403923
- /** @private @author ddj 2026年09月28号 Open all requested paths before acknowledging this exact lease. */
403924
- async function execute(open) {
403925
- executing = true;
403926
- try {
403927
- const success = await openDeepLink(ctx, open);
403928
- if (!disposed) await rpc("edrv.external.ack", {
403929
- token: open.token,
403930
- leaseId: open.leaseId,
403931
- clientId,
403932
- success,
403933
- error: success ? void 0 : "编辑器打开失败或取消"
403934
- });
403935
- } finally {
403936
- executing = false;
403937
- }
403938
- }
403939
- /** @private @author ddj 2026年09月28号 Poll while opening to renew the lease without accepting a second request. */
403940
- async function tick() {
403941
- if (disposed || polling) return;
403942
- polling = true;
403943
- try {
403944
- const result = await rpc("edrv.external.pending", {
403945
- clientId,
403946
- busy: executing
403947
- });
403948
- if (!disposed && result.ok && result.open && !executing) execute(result.open).catch(() => {});
403949
- } finally {
403950
- polling = false;
403951
- }
403952
- }
403953
- const timer = window.setInterval(() => {
403954
- if (hiddenSkip()) return;
403955
- tick().catch(() => {});
403956
- }, HANDOFF_POLL_MS);
403957
- /** @private @author ddj 2026年09月28号 Stop heartbeats and fail outstanding editor waits on plugin disposal. */
403958
- function dispose() {
403959
- disposed = true;
403960
- window.clearInterval(timer);
403961
- disposeOpenWait();
403962
- if (marker.__edrvExtPoll === dispose) delete marker.__edrvExtPoll;
403963
- }
403964
- marker.__edrvExtPoll = dispose;
403965
- tick().catch(() => {});
403966
- return dispose;
403967
- }
403968
- /**
403969
- * 跨源守卫:referrer 为空(launcher/Unity 直开)或同源 → 允许。
403970
- * @author ddj 2026年09月07号
403971
- * @param referrer 文档 referrer
403972
- * @param origin 当前页面源
403973
- * @returns 是否允许处理深链
403974
- */
403975
- function referrerAllowed(referrer, origin) {
403976
- if (!referrer) return true;
403977
- try {
403978
- return new URL(referrer).origin === origin;
403979
- } catch {
403980
- return false;
403981
- }
403982
- }
403983
- /**
403984
- * 剥离 URL 中的深链参数(保留其余参数与 hash,防刷新重开)。
403985
- * @author ddj 2026年09月07号
403986
- * @param href 完整 URL
403987
- * @returns 清理后的 URL(pathname+search+hash)
403988
- */
403989
- function stripOpenHref(href) {
403990
- const url = new URL(href);
403991
- for (const key of EDRV_PARAM_KEYS) url.searchParams.delete(key);
403992
- return url.pathname + url.search + url.hash;
403993
- }
403994
- /** 清理当前页面 URL(best-effort,失败不阻塞打开)。 */
403995
- function stripCurrentUrl() {
403996
- try {
403997
- if (typeof location === "undefined" || typeof history === "undefined") return;
403998
- const next = stripOpenHref(location.href);
403999
- if (next !== location.pathname + location.search + location.hash) history.replaceState(null, "", next);
404000
- } catch {}
404001
- }
404002
- /**
404003
- * 轻量 toast(编辑器未挂载也能提示;5s 自隐;openFlow 通知与错误呈现共用)。
404004
- * @author ddj 2026年09月07号
404005
- * @param text 提示文本
404006
- */
404007
- function toastDom(text) {
404008
- if (typeof document === "undefined") return;
404009
- const toast = document.createElement("div");
404010
- toast.textContent = text;
404011
- toast.setAttribute("style", [
404012
- "position:fixed",
404013
- "left:50%",
404014
- "bottom:28px",
404015
- "transform:translateX(-50%)",
404016
- "z-index:99999",
404017
- "padding:8px 14px",
404018
- "border-radius:8px",
404019
- "max-width:80vw",
404020
- "background:#2a2a2a",
404021
- "color:#ddd",
404022
- "border:1px solid #444",
404023
- "font-size:12px",
404024
- "box-shadow:0 4px 16px rgba(0,0,0,.35)"
404025
- ].join(";"));
404026
- document.body?.appendChild(toast);
404027
- setTimeout(() => toast.remove(), TOAST_MS);
404028
- }
404029
- //#endregion
404030
404510
  //#region src/client/compatSummary.ts
404031
404511
  /**
404032
404512
  * Classify the API generation without importing the host's node-only version probe.
@@ -404098,6 +404578,16 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
404098
404578
  active: state.fileClaim,
404099
404579
  note: state.fileClaim ? "聊天文件链接转发进单一编辑器页签" : "官方文件链接认领未安装"
404100
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
+ });
404101
404591
  return items;
404102
404592
  }
404103
404593
  //#endregion
@@ -404166,8 +404656,12 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
404166
404656
  const sessionId = props?.sessionId;
404167
404657
  const useTabInfo = props?.useTabInfo;
404168
404658
  let navigation;
404659
+ let tab;
404660
+ let visible = true;
404169
404661
  if (typeof useTabInfo === "function") try {
404170
- navigation = useTabInfo()?.tab?.navigation;
404662
+ tab = useTabInfo()?.tab;
404663
+ navigation = tab?.navigation;
404664
+ visible = tab?.visible !== false;
404171
404665
  } catch {
404172
404666
  navigation = void 0;
404173
404667
  }
@@ -404198,19 +404692,25 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
404198
404692
  if (revision <= 0) return;
404199
404693
  const fileHit = parseOfficialFileAddress(navigation?.address);
404200
404694
  if (fileHit) {
404201
- const line = navigation?.params?.line;
404695
+ const line = resolveNavLine(navigation?.params);
404202
404696
  window.dispatchEvent(new CustomEvent("edrv:open-editor", { detail: {
404697
+ sessionId,
404698
+ ownerId: tab?.id,
404203
404699
  path: fileHit.path,
404204
- line: typeof line === "number" ? line : void 0,
404700
+ line,
404701
+ column: resolveNavColumn(navigation?.params),
404205
404702
  preview: navigation?.params?.preview
404206
404703
  } }));
404207
404704
  return;
404208
404705
  }
404209
404706
  const request = resolveNavOpen(navigation?.params);
404210
404707
  window.dispatchEvent(new CustomEvent("edrv:open-editor", { detail: {
404708
+ sessionId,
404709
+ ownerId: tab?.id,
404211
404710
  path: request.path,
404212
404711
  focusDiff: request.focusDiff,
404213
404712
  line: resolveNavLine(navigation?.params),
404713
+ column: resolveNavColumn(navigation?.params),
404214
404714
  preview: navigation?.params?.preview
404215
404715
  } }));
404216
404716
  });
@@ -404218,11 +404718,15 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
404218
404718
  key: sessionId,
404219
404719
  sessionId,
404220
404720
  layout: "side",
404721
+ visible,
404722
+ ownerId: tab?.id,
404221
404723
  schedule: props?.schedule,
404222
404724
  addToConversation: props?.addToConversation,
404223
404725
  sidebarPanels: props?.sidebarPanels,
404224
404726
  outlineSources: props?.outlineSources,
404225
404727
  fileMenuItems: props?.fileMenuItems,
404728
+ officialMenus: props?.officialMenus,
404729
+ forge: props?.forge,
404226
404730
  fileOpeners: props?.fileOpeners,
404227
404731
  sessions: props?.sessions
404228
404732
  });
@@ -404289,17 +404793,27 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
404289
404793
  const [failed, setFailed] = react$1.default.useState(false);
404290
404794
  const doneRef = react$1.default.useRef(-1);
404291
404795
  react$1.default.useEffect(() => {
404292
- if (revision === doneRef.current) return;
404293
- 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;
404294
404803
  const parsed = parseOfficialFileAddress(navigation?.address);
404295
404804
  if (!parsed) {
404296
404805
  setFailed(true);
404297
404806
  return;
404298
404807
  }
404299
404808
  const line = resolveNavLine(navigation?.params);
404809
+ const column = resolveNavColumn(navigation?.params);
404810
+ let alive = true;
404811
+ setFailed(false);
404300
404812
  let left = FORWARD_ATTEMPTS;
404301
404813
  const attempt = () => {
404302
- 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;
404303
404817
  closeTab(tab);
404304
404818
  return;
404305
404819
  }
@@ -404308,7 +404822,15 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
404308
404822
  else setFailed(true);
404309
404823
  };
404310
404824
  attempt();
404311
- }, [revision]);
404825
+ return () => {
404826
+ alive = false;
404827
+ };
404828
+ }, [
404829
+ revision,
404830
+ props?.sessionId,
404831
+ tab?.id,
404832
+ navigation?.address
404833
+ ]);
404312
404834
  if (!failed || typeof fallback !== "function") return null;
404313
404835
  return react$1.default.createElement(fallback, props);
404314
404836
  }
@@ -408543,13 +409065,26 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
408543
409065
  function menu(event) {
408544
409066
  onContext(path, type, event);
408545
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
+ }
408546
409079
  return react$1.default.createElement("div", {
408547
409080
  className: "edrv-tree-row" + (active ? " edrv-tree-active" : "") + (context ? " edrv-tree-context" : "") + (reveal ? " edrv-tree-reveal" : "") + (mark?.stale ? " edrv-tree-stale" : ""),
408548
409081
  title: path + (mark?.stale ? ";SVN 状态陈旧,等待确认" : ""),
408549
409082
  "data-edrv-path": path,
408550
409083
  style: { paddingLeft: 6 + depth * 14 },
409084
+ tabIndex: 0,
408551
409085
  onClick: select,
408552
- onContextMenu: menu
409086
+ onContextMenu: menu,
409087
+ onKeyDown: menuKey
408553
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);
408554
409089
  }
408555
409090
  const ExplorerRow = react$1.default.memo(rowView);
@@ -409009,6 +409544,54 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
409009
409544
  if (menu && !menuValid) setMenu(null);
409010
409545
  }
409011
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
+ ]);
409012
409595
  const menuEntries = menuValid ? buildTreeMenu(ctx?.fileMenuItems, menu.target, ctx).map((item) => ({
409013
409596
  id: item.id,
409014
409597
  label: item.label,
@@ -412499,6 +413082,26 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
412499
413082
  function createDefaultFileMenuItems() {
412500
413083
  return [
412501
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
+ },
412502
413105
  ...revealGroupItems(),
412503
413106
  ...refGroupItems(),
412504
413107
  ...clipGroupItems(),
@@ -414340,7 +414943,9 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
414340
414943
  officialSidebar: officialService !== void 0,
414341
414944
  fileClaim: claimDisposer !== null,
414342
414945
  claimWanted: shouldClaimFiles(selected),
414343
- selected
414946
+ selected,
414947
+ officialMenus: Boolean(ctx.get("contextMenus")),
414948
+ forge: forge.available()
414344
414949
  }, version);
414345
414950
  ctx.provide("fileOpeners", registry);
414346
414951
  ctx.effect(() => registry.register(vscodeOpener()), "vscode-mode: file opener");
@@ -414372,12 +414977,17 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
414372
414977
  const sidebar = scanSidebar(ctx);
414373
414978
  return sidebar ? registry.register(sidebar) : void 0;
414374
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");
414375
414983
  const officialRenderTab = (props) => react$1.default.createElement(OfficialSideTab, Object.assign({}, props, {
414376
414984
  schedule,
414377
414985
  addToConversation,
414378
414986
  sidebarPanels,
414379
414987
  outlineSources,
414380
414988
  fileMenuItems,
414989
+ officialMenus: menuBridge,
414990
+ forge,
414381
414991
  fileOpeners: registry,
414382
414992
  sessions,
414383
414993
  readSessionScope: () => readSessionScope(ctx)
@@ -414562,6 +415172,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
414562
415172
  sidebarPanels,
414563
415173
  outlineSources,
414564
415174
  fileMenuItems,
415175
+ officialMenus: menuBridge,
415176
+ forge,
414565
415177
  fileOpeners: registry,
414566
415178
  sessions
414567
415179
  })));
@@ -414647,6 +415259,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
414647
415259
  sidebarPanels,
414648
415260
  outlineSources,
414649
415261
  fileMenuItems,
415262
+ officialMenus: menuBridge,
415263
+ forge,
414650
415264
  fileOpeners: registry,
414651
415265
  sessions
414652
415266
  })),
@@ -414756,6 +415370,17 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
414756
415370
  compatSummary,
414757
415371
  getShortcuts: () => detectShortcuts(ctx)
414758
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
+ });
414759
415384
  registerSlotSafely(ctx, {
414760
415385
  name: "plugins.detail.section",
414761
415386
  id: "edrv-plugin-status",