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.
- package/README.md +25 -11
- package/lib/client.js +1969 -1344
- package/lib/client.js.map +2 -2
- package/lib/index.js +1443 -716
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/compatSummary.ts +4 -0
- package/src/client/contextMenusOfficial.ts +80 -0
- package/src/client/forgeBridge.ts +28 -0
- package/src/client/index.ts +18 -3
- package/src/client/monaco/lsp/lspClient.ts +25 -7
- package/src/client/officialSidebar.ts +15 -3
- package/src/client/reviewBar.ts +3 -0
- package/src/client/reviewBatch.ts +9 -1
- package/src/client/reviewKeepAll.ts +72 -0
- package/src/client/sidebar/menuItems.ts +6 -0
- package/src/client/sidebar/panels/FileExplorer.ts +26 -1
- package/src/client/sidebar/types.ts +3 -0
- package/src/client/state/diffSummary.ts +34 -7
- package/src/client/state/recordsFeed.ts +40 -3
- package/src/client/state/reviewFeed.ts +9 -2
- package/src/client/tabMenu.ts +5 -0
- package/src/client/ui/ClaimRouter.ts +11 -5
- package/src/client/ui/EditorPane.ts +9 -1
- package/src/client/ui/EditorView.ts +164 -39
- package/src/client/ui/OfficialSideTab.ts +13 -5
- package/src/client/ui/ReviewAllConfirm.ts +1 -1
- package/src/index.ts +1 -1
- package/src/pending/capture.ts +96 -47
- package/src/pending/persist.ts +94 -5
- package/src/pending/store.ts +171 -58
- package/src/pendingCompat.ts +72 -9
- package/src/pendingRpc.ts +10 -10
- package/src/reviewCapture.ts +64 -23
- package/src/reviewDb.ts +119 -8
- package/src/reviewEvidence.ts +1 -1
- package/src/reviewRpc.ts +115 -9
- package/src/rpc.ts +14 -10
- package/src/shared/review.ts +21 -0
- package/src/shared/rpc.ts +3 -1
- package/src/store.ts +44 -0
- 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
|
-
|
|
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
|
|
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
|
-
/**
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
147698
|
+
return React$66.createElement(WrappedComponent, tslib_1.__assign({}, props, { children: renderTarget }));
|
|
146336
147699
|
}
|
|
146337
|
-
var Children = React$
|
|
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$
|
|
146349
|
-
var ref = React$
|
|
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$
|
|
147715
|
+
return React$66.createElement(React$66.Fragment, null, React$66.createElement(State, {
|
|
146353
147716
|
stateRef: ref,
|
|
146354
147717
|
props
|
|
146355
|
-
}), React$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
146461
|
-
var ref = React$
|
|
146462
|
-
var _a = React$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
148093
|
+
var gap = React$62.useMemo(function() {
|
|
146731
148094
|
return (0, utils_1.getGapWidth)(gapMode);
|
|
146732
148095
|
}, [gapMode]);
|
|
146733
|
-
return React$
|
|
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$
|
|
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$
|
|
146921
|
-
var touchStartRef = React$
|
|
146922
|
-
var activeAxis = React$
|
|
146923
|
-
var id = React$
|
|
146924
|
-
var Style = React$
|
|
146925
|
-
var lastProps = React$
|
|
146926
|
-
React$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
147075
|
-
return React$
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
385735
|
-
const
|
|
385736
|
-
|
|
385737
|
-
|
|
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
|
-
|
|
385741
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
395180
|
-
const timer = setInterval(
|
|
395181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
404293
|
-
|
|
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 (
|
|
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
|
-
|
|
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",
|