dsh-over-ssh 0.2.1 → 0.2.2
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 +12 -3
- package/agents/agent-manifest.json +4 -4
- package/agents/linux-amd64/dsh-over-ssh-agent +0 -0
- package/agents/linux-arm64/dsh-over-ssh-agent +0 -0
- package/lib/client.js +111 -40
- package/lib/file-references.js +17 -0
- package/lib/fs.js +87 -18
- package/lib/index.js +550 -145
- package/lib/terminal-wrapper.js +4 -3
- package/lib/types/src/host/app/session-admin/session-admin-contract.d.ts +11 -2
- package/lib/types/src/host/infra/transport/ssh/ssh2-transport.d.ts +8 -0
- package/lib/types/src/host/integration/fs/fs-provider.d.ts +9 -3
- package/lib/types/src/host/integration/session/session-header.d.ts +200 -0
- package/lib/types/src/wiring/helpers.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,6 +35,15 @@ shell:
|
|
|
35
35
|
|
|
36
36
|
不配也能用,只是 Web 终端永远开在本地(远程文件、远端执行、`@` 补全不受影响)。
|
|
37
37
|
|
|
38
|
+
## 0.2.2 更新
|
|
39
|
+
|
|
40
|
+
- 「添加目录」按会话作用域显示与删除,fork 子会话继承父会话的目录且立即可见。
|
|
41
|
+
- 会话面板:删除/还原/清理都有确认与部分失败说明;运行中的会话拒绝删除;回收站条目显示标题与删除时间。
|
|
42
|
+
- SSH 传输:连接闪断后自动重建(不再永久超时);版本守卫会校验摘要来源,损坏/缺失的摘要不再当守卫。
|
|
43
|
+
- 宿主 HTTP:请求体超限、路径含 NUL、根外访问返回 400 而不是 500;`?workspace=` 单独给出时也生效。
|
|
44
|
+
- 性能:会话面板刷新的日志头读取加签名缓存(31 会话冷 17 ms → 热 1.3–3.7 ms);`GET /roots` 与
|
|
45
|
+
`user-roots.json` 读取走 stat 短路;`?session=` 过滤先过滤后投影。
|
|
46
|
+
|
|
38
47
|
## 发布说明
|
|
39
48
|
|
|
40
49
|
包里带了两个平台的 Tier 2 代理二进制(纯静态,远端零依赖)。源码在配套的 Go 仓库
|
|
@@ -45,10 +54,10 @@ shell:
|
|
|
45
54
|
|
|
46
55
|
| 平台 | 字节 | sha256 |
|
|
47
56
|
|---|---|---|
|
|
48
|
-
| linux-amd64 | 4,944,032 | `
|
|
49
|
-
| linux-arm64 | 4,653,216 | `
|
|
57
|
+
| linux-amd64 | 4,944,032 | `d45237ef1bfec1955fb5e6a2391bd651ffeaffb89e781e1c518e9e9836eec974` |
|
|
58
|
+
| linux-arm64 | 4,653,216 | `237a3cc9f5ae57f752f62eee4f8a57bd8c4684fd4d3fa6ab5851115baa844ccd` |
|
|
50
59
|
|
|
51
|
-
- 代理版本:`0.2.
|
|
60
|
+
- 代理版本:`0.2.2-e019f2d`
|
|
52
61
|
- 源码提交:`e019f2d`(Go 仓 `dsh-over-ssh-agent` 的这个提交)
|
|
53
62
|
- 清单原文:包内 `agents/agent-manifest.json`
|
|
54
63
|
- 核对:`sha256sum agents/<平台>/dsh-over-ssh-agent` 与上表比对;按上面的提交检出 Go 仓后
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"generatedAt": "2026-11-
|
|
4
|
-
"agentVersion": "0.2.
|
|
3
|
+
"generatedAt": "2026-11-06T16:15:01.747Z",
|
|
4
|
+
"agentVersion": "0.2.2-e019f2d",
|
|
5
5
|
"sourceCommit": "e019f2d",
|
|
6
6
|
"artifacts": [
|
|
7
7
|
{
|
|
8
8
|
"platform": "linux-amd64",
|
|
9
9
|
"path": "linux-amd64/dsh-over-ssh-agent",
|
|
10
|
-
"sha256": "
|
|
10
|
+
"sha256": "d45237ef1bfec1955fb5e6a2391bd651ffeaffb89e781e1c518e9e9836eec974",
|
|
11
11
|
"bytes": 4944032
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
"platform": "linux-arm64",
|
|
15
15
|
"path": "linux-arm64/dsh-over-ssh-agent",
|
|
16
|
-
"sha256": "
|
|
16
|
+
"sha256": "237a3cc9f5ae57f752f62eee4f8a57bd8c4684fd4d3fa6ab5851115baa844ccd",
|
|
17
17
|
"bytes": 4653216
|
|
18
18
|
}
|
|
19
19
|
]
|
|
Binary file
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -1698,8 +1698,8 @@ function parseRootsSnapshot(value) {
|
|
|
1698
1698
|
const row = toRootRow(entry, "config", false);
|
|
1699
1699
|
const identity = row === void 0 ? void 0 : rootKeyIdentity(row.key);
|
|
1700
1700
|
if (row !== void 0 && identity !== void 0 && !seen.has(identity)) {
|
|
1701
|
-
const scoped = userWorkspaces.get(identity);
|
|
1702
|
-
const scopedSessions = userSessions.get(identity);
|
|
1701
|
+
const scoped = row.workspaces ?? userWorkspaces.get(identity);
|
|
1702
|
+
const scopedSessions = row.sessions ?? userSessions.get(identity);
|
|
1703
1703
|
roots.push({
|
|
1704
1704
|
...row,
|
|
1705
1705
|
inUserFile: userKeys.has(identity),
|
|
@@ -1723,7 +1723,14 @@ function parseRootsSnapshot(value) {
|
|
|
1723
1723
|
}
|
|
1724
1724
|
const warnings = (Array.isArray(payload.warnings) ? payload.warnings : []).filter((item) => typeof item === "string");
|
|
1725
1725
|
const anchorRoot = readString(payload, "anchorRoot");
|
|
1726
|
-
|
|
1726
|
+
const scopedForSession = readString(payload, "scopedForSession");
|
|
1727
|
+
return {
|
|
1728
|
+
roots,
|
|
1729
|
+
connections,
|
|
1730
|
+
warnings,
|
|
1731
|
+
...anchorRoot === void 0 ? {} : { anchorRoot },
|
|
1732
|
+
...scopedForSession === void 0 ? {} : { scopedForSession }
|
|
1733
|
+
};
|
|
1727
1734
|
}
|
|
1728
1735
|
function isAbsoluteRemotePath(path) {
|
|
1729
1736
|
if (!path.startsWith("/")) return false;
|
|
@@ -1912,13 +1919,27 @@ async function requestSnapshot(fetchImpl, path, init) {
|
|
|
1912
1919
|
const reason = typeof body?.error === "string" ? body.error : transportFailureReason(response.status, path);
|
|
1913
1920
|
return { ok: false, error: reason };
|
|
1914
1921
|
}
|
|
1922
|
+
if (typeof body === "object" && body !== null && body["ok"] === false && typeof body["error"] === "string") {
|
|
1923
|
+
return { ok: false, error: body["error"] };
|
|
1924
|
+
}
|
|
1915
1925
|
return { ok: true, value: parseRootsSnapshot(body) };
|
|
1916
1926
|
} catch (error) {
|
|
1917
1927
|
return { ok: false, error: error instanceof Error ? error.name : "network" };
|
|
1918
1928
|
}
|
|
1919
1929
|
}
|
|
1920
1930
|
function createRootsApi(fetchImpl, path = ROOTS_API_PATH) {
|
|
1921
|
-
const load = async (
|
|
1931
|
+
const load = async (options = {}) => {
|
|
1932
|
+
const normalized = options instanceof AbortSignal ? { signal: options } : options;
|
|
1933
|
+
const query = new URLSearchParams();
|
|
1934
|
+
if (normalized.session !== void 0 && normalized.session !== "") query.set("session", normalized.session);
|
|
1935
|
+
if (normalized.workspace !== void 0 && normalized.workspace !== "") query.set("workspace", normalized.workspace);
|
|
1936
|
+
const suffix = query.size === 0 ? "" : `?${query.toString()}`;
|
|
1937
|
+
return requestSnapshot(fetchImpl, `${path}${suffix}`, {
|
|
1938
|
+
method: "GET",
|
|
1939
|
+
headers: { accept: "application/json" },
|
|
1940
|
+
...normalized.signal === void 0 ? {} : { signal: normalized.signal }
|
|
1941
|
+
});
|
|
1942
|
+
};
|
|
1922
1943
|
const send = async (payload) => requestSnapshot(fetchImpl, path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) });
|
|
1923
1944
|
const sendRaw = async (payload) => {
|
|
1924
1945
|
try {
|
|
@@ -2435,7 +2456,16 @@ function createTransferProgress(options) {
|
|
|
2435
2456
|
setState(transferPanelPollStarted(state, showLoading));
|
|
2436
2457
|
const options_ = mode === "park" ? { waitMs, signal: controller.signal } : { signal: controller.signal };
|
|
2437
2458
|
issued += 1;
|
|
2438
|
-
|
|
2459
|
+
let result;
|
|
2460
|
+
try {
|
|
2461
|
+
result = await options.io.list(options_);
|
|
2462
|
+
} catch (error) {
|
|
2463
|
+
if (controller.signal.aborted || disposed) {
|
|
2464
|
+
if (inFlight === controller) inFlight = void 0;
|
|
2465
|
+
continue;
|
|
2466
|
+
}
|
|
2467
|
+
result = { ok: false, error: error instanceof Error ? error.message : "list-failed" };
|
|
2468
|
+
}
|
|
2439
2469
|
if (inFlight === controller) inFlight = void 0;
|
|
2440
2470
|
if (disposed) return;
|
|
2441
2471
|
if (mine !== generation) {
|
|
@@ -2476,6 +2506,7 @@ function createTransferProgress(options) {
|
|
|
2476
2506
|
},
|
|
2477
2507
|
clear: () => {
|
|
2478
2508
|
if (disposed) return;
|
|
2509
|
+
if (state.clearing) return;
|
|
2479
2510
|
setState(transferPanelClearing(state));
|
|
2480
2511
|
void options.io.clear().then((result) => {
|
|
2481
2512
|
if (disposed) return;
|
|
@@ -2574,7 +2605,9 @@ var ROOTS_EDIT_BACKDROP_CLASS = classes(CLASS.backdrop, CLASS.backdropSettings);
|
|
|
2574
2605
|
function createRootsPanelController(options) {
|
|
2575
2606
|
let state = initialRootsPanelState();
|
|
2576
2607
|
const listeners = /* @__PURE__ */ new Set();
|
|
2577
|
-
let
|
|
2608
|
+
let readToken = 0;
|
|
2609
|
+
let writeToken = 0;
|
|
2610
|
+
let epoch = 0;
|
|
2578
2611
|
let disposed = false;
|
|
2579
2612
|
const requests = createAbortGate();
|
|
2580
2613
|
const dispatch = (action) => {
|
|
@@ -2583,22 +2616,30 @@ function createRootsPanelController(options) {
|
|
|
2583
2616
|
state = next;
|
|
2584
2617
|
for (const listener of [...listeners]) listener();
|
|
2585
2618
|
};
|
|
2586
|
-
const
|
|
2619
|
+
const latestRead = (mine, issuedEpoch) => !disposed && mine === readToken && issuedEpoch === epoch;
|
|
2620
|
+
const latestWrite = (mine) => !disposed && mine === writeToken;
|
|
2587
2621
|
const reload = async () => {
|
|
2588
|
-
const mine = ++
|
|
2622
|
+
const mine = ++readToken;
|
|
2623
|
+
const issuedEpoch = epoch;
|
|
2589
2624
|
const signal = requests.begin();
|
|
2590
2625
|
dispatch({ type: "load" });
|
|
2591
2626
|
const result = await options.api.load(signal);
|
|
2592
|
-
if (!
|
|
2627
|
+
if (!latestRead(mine, issuedEpoch)) return;
|
|
2628
|
+
if (result.ok && result.value.warnings.includes("roots-invalid-answer")) {
|
|
2629
|
+
dispatch({ type: "failed", error: "roots-invalid-answer" });
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2593
2632
|
if (result.ok) dispatch({ type: "loaded", snapshot: result.value });
|
|
2594
2633
|
else dispatch({ type: "failed", error: result.error });
|
|
2595
2634
|
};
|
|
2596
2635
|
const settle = async (mine, scope, result, resetDrafts) => {
|
|
2597
|
-
if (!
|
|
2636
|
+
if (!latestWrite(mine)) return;
|
|
2598
2637
|
if (!result.ok) {
|
|
2599
2638
|
dispatch({ type: "failed", error: result.error, scope });
|
|
2600
2639
|
return;
|
|
2601
2640
|
}
|
|
2641
|
+
epoch += 1;
|
|
2642
|
+
void result.value;
|
|
2602
2643
|
dispatch({ type: "succeeded", scope, ...resetDrafts ? { resetDrafts: true } : {} });
|
|
2603
2644
|
await reload();
|
|
2604
2645
|
};
|
|
@@ -2611,7 +2652,7 @@ function createRootsPanelController(options) {
|
|
|
2611
2652
|
dispatch({ type: "refused", scope: "roots", message: validated.message });
|
|
2612
2653
|
return;
|
|
2613
2654
|
}
|
|
2614
|
-
const mine = ++
|
|
2655
|
+
const mine = ++writeToken;
|
|
2615
2656
|
dispatch({ type: "submitted" });
|
|
2616
2657
|
const result = await options.api.send({ action: "add-root", ...validated.value });
|
|
2617
2658
|
await settle(mine, "roots", result, true);
|
|
@@ -2637,7 +2678,7 @@ function createRootsPanelController(options) {
|
|
|
2637
2678
|
dispatch({ type: "refused", scope: "connection", message: validated.message });
|
|
2638
2679
|
return;
|
|
2639
2680
|
}
|
|
2640
|
-
const mine = ++
|
|
2681
|
+
const mine = ++writeToken;
|
|
2641
2682
|
dispatch({ type: "submitted" });
|
|
2642
2683
|
const result = await options.api.send({ action: "add-connection", ...validated.value });
|
|
2643
2684
|
await settle(mine, "connection", result, true);
|
|
@@ -2646,7 +2687,7 @@ function createRootsPanelController(options) {
|
|
|
2646
2687
|
const row = selectRootsView(state).rows.find((candidate) => candidate.key === key);
|
|
2647
2688
|
if (row === void 0 || !row.removable) return;
|
|
2648
2689
|
if (!canEdit(state)) return;
|
|
2649
|
-
const mine = ++
|
|
2690
|
+
const mine = ++writeToken;
|
|
2650
2691
|
dispatch({ type: "remove", key });
|
|
2651
2692
|
const result = await options.api.send({ action: "remove-root", key });
|
|
2652
2693
|
await settle(mine, "roots", result, false);
|
|
@@ -2702,7 +2743,7 @@ function createRootsPanelController(options) {
|
|
|
2702
2743
|
dispatch({ type: "refused", scope: "edit", message: validated.message });
|
|
2703
2744
|
return;
|
|
2704
2745
|
}
|
|
2705
|
-
const mine = ++
|
|
2746
|
+
const mine = ++writeToken;
|
|
2706
2747
|
dispatch({ type: "submitted" });
|
|
2707
2748
|
const result = await options.api.send({ action: "update-connection", id: editingId, ...validated.value });
|
|
2708
2749
|
await settle(mine, "edit", result, true);
|
|
@@ -2716,16 +2757,16 @@ function createRootsPanelController(options) {
|
|
|
2716
2757
|
const confirmRemoveConnection = async (id) => {
|
|
2717
2758
|
if (state.confirmingRemove !== id) return;
|
|
2718
2759
|
if (!canEdit(state)) return;
|
|
2719
|
-
const mine = ++
|
|
2760
|
+
const mine = ++writeToken;
|
|
2720
2761
|
dispatch({ type: "submitted" });
|
|
2721
2762
|
const result = await options.api.send({ action: "remove-connection", id });
|
|
2722
2763
|
await settle(mine, "edit", result, true);
|
|
2723
2764
|
};
|
|
2724
2765
|
const loadImport = async () => {
|
|
2725
|
-
const mine = ++
|
|
2766
|
+
const mine = ++readToken;
|
|
2726
2767
|
dispatch({ type: "import-busy" });
|
|
2727
2768
|
const result = await options.api.sendRaw({ action: "list-ssh-config" });
|
|
2728
|
-
if (
|
|
2769
|
+
if (disposed || mine !== readToken) return;
|
|
2729
2770
|
if (!result.ok) {
|
|
2730
2771
|
dispatch({ type: "import-failed", error: result.error });
|
|
2731
2772
|
return;
|
|
@@ -2739,10 +2780,10 @@ function createRootsPanelController(options) {
|
|
|
2739
2780
|
const submitImport = async () => {
|
|
2740
2781
|
const selected = state.importSelected ?? [];
|
|
2741
2782
|
if (selected.length === 0) return;
|
|
2742
|
-
const mine = ++
|
|
2783
|
+
const mine = ++writeToken;
|
|
2743
2784
|
dispatch({ type: "import-busy" });
|
|
2744
2785
|
const result = await options.api.sendRaw({ action: "import-ssh-config", aliases: selected });
|
|
2745
|
-
if (!
|
|
2786
|
+
if (!latestWrite(mine)) return;
|
|
2746
2787
|
if (!result.ok) {
|
|
2747
2788
|
dispatch({ type: "import-failed", error: result.error });
|
|
2748
2789
|
return;
|
|
@@ -3625,10 +3666,10 @@ function buildPanelModel(copy, options = {}) {
|
|
|
3625
3666
|
// src/shared/remote-identity.ts
|
|
3626
3667
|
var SSH_DEV_REMOTE_NAMESPACE = "sshdev";
|
|
3627
3668
|
var SSH_DEV_REMOTE_SERVICE = "sshDevRemote";
|
|
3628
|
-
|
|
3629
|
-
// src/client/core/remote.ts
|
|
3630
3669
|
var SSH_DEV_REMOTE_METHOD = "connections";
|
|
3631
3670
|
var SSH_DEV_REMOTE_PACKAGE = "dsh-over-ssh";
|
|
3671
|
+
|
|
3672
|
+
// src/client/core/remote.ts
|
|
3632
3673
|
var SSH_DEV_CONNECTIONS_DESCRIPTOR = {
|
|
3633
3674
|
id: `${SSH_DEV_REMOTE_PACKAGE}#${SSH_DEV_REMOTE_NAMESPACE}/${SSH_DEV_REMOTE_METHOD}`,
|
|
3634
3675
|
service: SSH_DEV_REMOTE_SERVICE,
|
|
@@ -4526,6 +4567,7 @@ function createSessionRowActionStore(deps) {
|
|
|
4526
4567
|
};
|
|
4527
4568
|
});
|
|
4528
4569
|
let cancelAutoClose;
|
|
4570
|
+
let postInFlight = false;
|
|
4529
4571
|
const stopAutoClose = () => {
|
|
4530
4572
|
cancelAutoClose?.();
|
|
4531
4573
|
cancelAutoClose = void 0;
|
|
@@ -4550,18 +4592,21 @@ function createSessionRowActionStore(deps) {
|
|
|
4550
4592
|
const confirm = async () => {
|
|
4551
4593
|
const request = state.request;
|
|
4552
4594
|
const row = state.row;
|
|
4553
|
-
if (disposed || request === void 0 || state.busy) return;
|
|
4595
|
+
if (disposed || request === void 0 || state.busy || postInFlight) return;
|
|
4554
4596
|
if (state.phase !== "ready" || row === void 0 || !row.deletable) return;
|
|
4555
4597
|
const stillPending = () => {
|
|
4556
4598
|
const current = state.request;
|
|
4557
4599
|
return current !== void 0 && current.kind === request.kind && current.sessionId === request.sessionId;
|
|
4558
4600
|
};
|
|
4601
|
+
postInFlight = true;
|
|
4559
4602
|
dispatch({ ...state, busy: true, outcome: void 0 });
|
|
4560
4603
|
let result;
|
|
4561
4604
|
try {
|
|
4562
4605
|
result = await deleteSessions(deps.io, [request.sessionId], request.kind === "delete" ? { permanent: true } : {});
|
|
4563
4606
|
} catch {
|
|
4564
4607
|
result = { kind: "error", message: { key: "sessionResultActionFailed" } };
|
|
4608
|
+
} finally {
|
|
4609
|
+
postInFlight = false;
|
|
4565
4610
|
}
|
|
4566
4611
|
if (disposed || !stillPending()) return;
|
|
4567
4612
|
dispatch({ ...state, busy: false, outcome: result });
|
|
@@ -4584,7 +4629,7 @@ function createSessionRowActionStore(deps) {
|
|
|
4584
4629
|
request: (request) => {
|
|
4585
4630
|
if (disposed) return;
|
|
4586
4631
|
stopAutoClose();
|
|
4587
|
-
dispatch({ request, phase: "loading", row: void 0, busy:
|
|
4632
|
+
dispatch({ request, phase: "loading", row: void 0, busy: postInFlight, outcome: void 0 });
|
|
4588
4633
|
},
|
|
4589
4634
|
load,
|
|
4590
4635
|
confirm,
|
|
@@ -5281,7 +5326,7 @@ function DirectoryBrowserSection({
|
|
|
5281
5326
|
listRef
|
|
5282
5327
|
}) {
|
|
5283
5328
|
const view = selectDirectoryBrowserView(state);
|
|
5284
|
-
const gate = disabled;
|
|
5329
|
+
const gate = disabled || state.pending;
|
|
5285
5330
|
const connectionFieldId = `${idPrefix}-connection`;
|
|
5286
5331
|
const modeButton = (mode, label) => Button({
|
|
5287
5332
|
// The active world is the brand-filled one: the switch reads without an extra attribute, and both
|
|
@@ -5503,12 +5548,18 @@ function createSessionDirectoryIo(options) {
|
|
|
5503
5548
|
});
|
|
5504
5549
|
const rootsApi = createRootsApi(options.fetchImpl, path);
|
|
5505
5550
|
const loadRoots = async (workspacePath, sessionId) => {
|
|
5506
|
-
const result = await rootsApi.load(
|
|
5551
|
+
const result = await rootsApi.load({
|
|
5552
|
+
...sessionId === void 0 || sessionId === "" ? {} : { session: sessionId },
|
|
5553
|
+
...workspacePath === void 0 || workspacePath === "" ? {} : { workspace: workspacePath }
|
|
5554
|
+
});
|
|
5507
5555
|
if (!result.ok) return result;
|
|
5508
5556
|
if (result.value.warnings.includes("roots-invalid-answer")) {
|
|
5509
5557
|
return { ok: false, error: "roots-invalid-answer" };
|
|
5510
5558
|
}
|
|
5511
5559
|
const all = result.value.roots;
|
|
5560
|
+
if (sessionId !== void 0 && sessionId !== "" && result.value.scopedForSession === sessionId) {
|
|
5561
|
+
return { ok: true, value: all };
|
|
5562
|
+
}
|
|
5512
5563
|
if (workspacePath === void 0 || workspacePath === "") return { ok: true, value: [] };
|
|
5513
5564
|
return {
|
|
5514
5565
|
ok: true,
|
|
@@ -5651,7 +5702,11 @@ function selectSessionDirectoryView(state, copy) {
|
|
|
5651
5702
|
title: row.label ?? (row.location === "remote" ? `${row.connectionId ?? ""}:${row.path}` : row.path),
|
|
5652
5703
|
subtitle: row.label === void 0 ? "" : row.path,
|
|
5653
5704
|
badge: row.location === "remote" ? copy["rootsBadgeRemote"] : copy["rootsBadgeLocal"],
|
|
5654
|
-
|
|
5705
|
+
// fork 继承来的行没有删除按钮:它在父亲那里才是"本会话添加的",在这里点删除会把父会话的
|
|
5706
|
+
// 目录拿走(宿主也会拒绝)。没有 sessions 字段的历史条目按文件权限判定,保持旧行为。
|
|
5707
|
+
removable: row.inUserFile && // 会话型条目只有**本会话**能删;`state.sessionId` 还没到也不能放行(那会把父会话的目录
|
|
5708
|
+
// 交给 fork 去删,宿主会拒,用户看到的是一次"按了没反应")。历史条目(无 sessions)按文件权限。
|
|
5709
|
+
(row.sessions === void 0 || state.sessionId !== void 0 && row.sessions.includes(state.sessionId))
|
|
5655
5710
|
}))
|
|
5656
5711
|
],
|
|
5657
5712
|
rootsPending: state.rootsPending,
|
|
@@ -5706,11 +5761,14 @@ function createSessionDirectory(deps) {
|
|
|
5706
5761
|
const commit = async (path, connectionId) => {
|
|
5707
5762
|
if (disposed || state.pending) return;
|
|
5708
5763
|
const location = connectionId === void 0 ? "local" : "remote";
|
|
5709
|
-
const validated = validateRootDraft(
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5764
|
+
const validated = validateRootDraft(
|
|
5765
|
+
{
|
|
5766
|
+
location,
|
|
5767
|
+
path,
|
|
5768
|
+
...connectionId === void 0 ? {} : { connectionId }
|
|
5769
|
+
},
|
|
5770
|
+
state.roots
|
|
5771
|
+
);
|
|
5714
5772
|
if (!validated.ok) {
|
|
5715
5773
|
dispatch({ type: "refused", message: validated.message });
|
|
5716
5774
|
return;
|
|
@@ -5819,7 +5877,10 @@ function SessionDirectorySection({
|
|
|
5819
5877
|
{ className: CLASS.block },
|
|
5820
5878
|
(0, import_react11.createElement)("h4", { className: CLASS.blockTitle }, copy.sessionRootsTitle),
|
|
5821
5879
|
(0, import_react11.createElement)("p", { className: CLASS.hint }, copy.sessionRootsHint),
|
|
5822
|
-
|
|
5880
|
+
// **有行就画列表**:工作区那一行是合成行,它不属于"已添加目录",但它是这张卡片最重要的一行。
|
|
5881
|
+
// 旧条件按"有没有已添加目录"决定画不画整张列表,结果是新会话只看到「还没有添加目录」,连会话
|
|
5882
|
+
// 自己在哪个工作区都不显示,必须先去添加一个目录才冒出来(审计 2026-11-06,用户截图)。
|
|
5883
|
+
view.roots.length === 0 ? view.rootsReading ? Notice({ role: "status", tone: "info", children: copy.sessionRootsLoading }) : (
|
|
5823
5884
|
// A failed read is not an empty list: the error notice below is the whole truth there, and
|
|
5824
5885
|
// 「还没有添加目录」 next to it would contradict it (same rule as the import dialog, ui P2-2/P2-3).
|
|
5825
5886
|
view.notice === "" ? (0, import_react11.createElement)("p", { className: CLASS.empty }, copy.sessionRootsEmpty) : null
|
|
@@ -5839,7 +5900,9 @@ function SessionDirectorySection({
|
|
|
5839
5900
|
Button({
|
|
5840
5901
|
variant: "ghost",
|
|
5841
5902
|
size: "compact",
|
|
5842
|
-
|
|
5903
|
+
// 刷新在飞时也禁用:否则一次点击会叠加"删除 + 一次刷新"两次远端写
|
|
5904
|
+
// (审计 2026-11-06 客户端 P2)。
|
|
5905
|
+
disabled: view.rootsPending || view.rootsReading,
|
|
5843
5906
|
onClick: () => actions.onRemoveRoot(row.key),
|
|
5844
5907
|
children: copy.rootsRemove
|
|
5845
5908
|
})
|
|
@@ -5847,6 +5910,8 @@ function SessionDirectorySection({
|
|
|
5847
5910
|
)
|
|
5848
5911
|
)
|
|
5849
5912
|
),
|
|
5913
|
+
// 列表里只剩工作区行时,"还没有添加目录"仍然是有用的下一步提示;但它不再替换列表。
|
|
5914
|
+
view.roots.length > 0 && view.roots.filter((row) => row.workspace !== true).length === 0 && !view.rootsReading && view.notice === "" ? (0, import_react11.createElement)("p", { className: CLASS.empty }, copy.sessionRootsEmpty) : null,
|
|
5850
5915
|
(0, import_react11.createElement)(
|
|
5851
5916
|
"div",
|
|
5852
5917
|
{ className: CLASS.actions },
|
|
@@ -5886,20 +5951,22 @@ function readWorkspacePath(props) {
|
|
|
5886
5951
|
const fromWorkspace = workspaces === void 0 ? void 0 : workspaces(
|
|
5887
5952
|
(snapshot) => sessionId === void 0 || sessionId === "" ? void 0 : snapshot?.items?.find((item) => item.sessionIds?.includes(sessionId) === true)?.path
|
|
5888
5953
|
);
|
|
5889
|
-
if (typeof fromSession === "string" && fromSession !== "") return fromSession;
|
|
5890
5954
|
if (typeof fromWorkspace === "string" && fromWorkspace !== "") return fromWorkspace;
|
|
5955
|
+
if (typeof fromSession === "string" && fromSession !== "") return fromSession;
|
|
5891
5956
|
return void 0;
|
|
5892
5957
|
}
|
|
5893
5958
|
function rootsQueryKey(state) {
|
|
5894
5959
|
if (!state.open) return "closed";
|
|
5895
|
-
|
|
5896
|
-
|
|
5960
|
+
const workspace = state.workspacePath === void 0 || state.workspacePath === "" ? "unknown" : state.workspacePath;
|
|
5961
|
+
const session = state.sessionId === void 0 || state.sessionId === "" ? "unknown" : state.sessionId;
|
|
5962
|
+
return `open:${workspace}\0${session}`;
|
|
5897
5963
|
}
|
|
5898
5964
|
function SessionDirectoryContainer(props) {
|
|
5899
5965
|
const { copy, io } = props;
|
|
5900
5966
|
const copyRef = (0, import_react11.useRef)(copy);
|
|
5901
5967
|
copyRef.current = copy;
|
|
5902
|
-
const
|
|
5968
|
+
const injected = props.controller;
|
|
5969
|
+
const [controller] = (0, import_react11.useState)(() => injected ?? createSessionDirectory({ io, copy: () => copyRef.current }));
|
|
5903
5970
|
const state = (0, import_react11.useSyncExternalStore)(controller.subscribe, controller.getState);
|
|
5904
5971
|
const workspacePath = readWorkspacePath(props);
|
|
5905
5972
|
const titleId = (0, import_react11.useId)();
|
|
@@ -5924,8 +5991,10 @@ function SessionDirectoryContainer(props) {
|
|
|
5924
5991
|
useCardTabTrap(state.open, portal.container);
|
|
5925
5992
|
(0, import_react11.useEffect)(() => {
|
|
5926
5993
|
controller.activate();
|
|
5927
|
-
return () =>
|
|
5928
|
-
|
|
5994
|
+
return () => {
|
|
5995
|
+
if (injected === void 0) controller.dispose();
|
|
5996
|
+
};
|
|
5997
|
+
}, [controller, injected]);
|
|
5929
5998
|
(0, import_react11.useLayoutEffect)(() => {
|
|
5930
5999
|
if (!state.open) return;
|
|
5931
6000
|
focusOrigin.current = captureFocusOrigin(browserPortalDocument());
|
|
@@ -6889,6 +6958,7 @@ function apply(ctx) {
|
|
|
6889
6958
|
return await chooser.pickDirectory();
|
|
6890
6959
|
}
|
|
6891
6960
|
});
|
|
6961
|
+
const controller = createSessionDirectory({ io, copy: () => copy() });
|
|
6892
6962
|
const registerSessionEntry = () => ctx.slots.inject(
|
|
6893
6963
|
SESSION_DIRECTORY_SLOT,
|
|
6894
6964
|
() => ctx.slots.register(
|
|
@@ -6896,7 +6966,7 @@ function apply(ctx) {
|
|
|
6896
6966
|
name: SESSION_DIRECTORY_SLOT,
|
|
6897
6967
|
id: SESSION_DIRECTORY_ID,
|
|
6898
6968
|
order: SESSION_DIRECTORY_ORDER,
|
|
6899
|
-
inject: () => ({ copy: copy(), io })
|
|
6969
|
+
inject: () => ({ copy: copy(), io, controller })
|
|
6900
6970
|
},
|
|
6901
6971
|
SessionDirectoryContainer
|
|
6902
6972
|
)
|
|
@@ -6909,6 +6979,7 @@ function apply(ctx) {
|
|
|
6909
6979
|
return () => {
|
|
6910
6980
|
offLocaleChange();
|
|
6911
6981
|
disposeEntry();
|
|
6982
|
+
controller.dispose();
|
|
6912
6983
|
};
|
|
6913
6984
|
});
|
|
6914
6985
|
ctx.effect(() => {
|
package/lib/file-references.js
CHANGED
|
@@ -154,11 +154,28 @@ function normalizeLocalPath2(path, options = {}) {
|
|
|
154
154
|
return normalizeLocalPath(path, { caseInsensitive: options.caseInsensitive ?? process.platform === "win32" });
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
// src/host/integration/session-fork-pending.ts
|
|
158
|
+
var pending = /* @__PURE__ */ new Map();
|
|
159
|
+
function pendingForkSources(sessionId) {
|
|
160
|
+
if (sessionId === void 0 || sessionId === "") return [];
|
|
161
|
+
const source = pending.get(sessionId);
|
|
162
|
+
return source === void 0 ? [] : [source];
|
|
163
|
+
}
|
|
164
|
+
|
|
157
165
|
// src/host/integration/session-scope.ts
|
|
158
166
|
var MAX_FORK_DEPTH = 32;
|
|
159
167
|
var ancestorCache = /* @__PURE__ */ new Map();
|
|
160
168
|
var ANCESTOR_CACHE_MAX = 256;
|
|
161
169
|
function readSessionAncestors(ctx, sessionId) {
|
|
170
|
+
const pending2 = pendingForkSources(sessionId);
|
|
171
|
+
if (pending2.length > 0) {
|
|
172
|
+
const inherited = /* @__PURE__ */ new Set();
|
|
173
|
+
for (const source of pending2) {
|
|
174
|
+
inherited.add(source);
|
|
175
|
+
for (const ancestor of walkAncestors(ctx, source).ancestors) inherited.add(ancestor);
|
|
176
|
+
}
|
|
177
|
+
return [...inherited];
|
|
178
|
+
}
|
|
162
179
|
const cached = ancestorCache.get(sessionId);
|
|
163
180
|
if (cached !== void 0) {
|
|
164
181
|
ancestorCache.delete(sessionId);
|