dsh-over-ssh 0.2.1 → 0.2.3
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 +42 -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/cordis.patch.yml +10 -12
- package/lib/client.js +194 -62
- package/lib/file-references.js +153 -13
- package/lib/fs.js +87 -18
- package/lib/index.js +710 -179
- package/lib/terminal-wrapper.js +4 -3
- package/lib/types/src/host/app/session-admin/session-admin-contract.d.ts +28 -2
- package/lib/types/src/host/domain/session/session-admin.d.ts +8 -0
- 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/local-bridge.d.ts +4 -4
- package/lib/types/src/wiring/helpers.d.ts +1 -1
- package/package.json +1 -5
- package/lib/local-bridge-refs.js +0 -16
- package/lib/types/src/local-bridge-refs.d.ts +0 -25
package/lib/client.js
CHANGED
|
@@ -82,6 +82,8 @@ var OVER_SSH_DICTS = {
|
|
|
82
82
|
sessionPanelSessions: "\u4F1A\u8BDD",
|
|
83
83
|
sessionPanelBin: "\u56DE\u6536\u7AD9",
|
|
84
84
|
sessionPanelBinEmpty: "\u56DE\u6536\u7AD9\u4E3A\u7A7A\u3002",
|
|
85
|
+
sessionPanelBinUnreadable: "\u53E6\u6709 {count} \u6761\u56DE\u6536\u7AD9\u6761\u76EE\u65E0\u6CD5\u663E\u793A\uFF08\u7F3A\u5C11\u4F1A\u8BDD\u6807\u8BC6\uFF09\uFF1B\u53EF\u91CD\u542F\u5BBF\u4E3B\u6216\u68C0\u67E5\u56DE\u6536\u7AD9\u7D22\u5F15\u3002",
|
|
86
|
+
sessionPanelBinRowUnaddressable: "\u8BE5\u6761\u76EE\u7F3A\u5C11\u6807\u8BC6\uFF0C\u65E0\u6CD5\u8FD8\u539F\u6216\u6E05\u9664",
|
|
85
87
|
sessionPanelSelectAll: "\u5168\u9009\u53EF\u5220\u7684",
|
|
86
88
|
sessionPanelClear: "\u6E05\u7A7A\u9009\u62E9",
|
|
87
89
|
sessionPanelDeleteRecycle: "\u5220\u9664\u5230\u56DE\u6536\u7AD9",
|
|
@@ -159,6 +161,7 @@ var OVER_SSH_DICTS = {
|
|
|
159
161
|
rootsErrorIdRequired: "\u8BF7\u586B\u5199\u8FDE\u63A5 id\u3002",
|
|
160
162
|
rootsErrorIdInvalid: "\u8FDE\u63A5 id \u53EA\u80FD\u5305\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u70B9\u3001\u4E0B\u5212\u7EBF\u548C\u77ED\u6A2A\u7EBF\uFF0C\u4E14\u5FC5\u987B\u4EE5\u5B57\u6BCD\u6216\u6570\u5B57\u5F00\u5934\u3002",
|
|
161
163
|
rootsErrorIdDuplicate: "\u8FD9\u4E2A\u8FDE\u63A5 id \u5DF2\u7ECF\u5B58\u5728\u3002",
|
|
164
|
+
rootsErrorConnectionMissing: "\u8BE5\u8FDE\u63A5\u5DF2\u88AB\u5220\u9664\uFF0C\u65E0\u6CD5\u4FDD\u5B58\uFF1B\u8BF7\u5173\u95ED\u7F16\u8F91\u5E76\u5237\u65B0\u5217\u8868\u3002",
|
|
162
165
|
rootsErrorHostRequired: "\u8BF7\u586B\u5199\u4E3B\u673A\u540D\u3002",
|
|
163
166
|
rootsErrorUserRequired: "\u8BF7\u586B\u5199\u7528\u6237\u540D\u3002",
|
|
164
167
|
rootsErrorPort: "\u7AEF\u53E3\u5FC5\u987B\u662F 1 \u5230 65535 \u4E4B\u95F4\u7684\u6574\u6570\u3002",
|
|
@@ -276,6 +279,9 @@ var OVER_SSH_DICTS = {
|
|
|
276
279
|
sessionResultWorkspaceRecycled: "\u5DF2\u628A\u8BE5\u5DE5\u4F5C\u533A {count} \u4E2A\u4F1A\u8BDD\u79FB\u5165\u56DE\u6536\u7AD9\uFF0C{size}\uFF08\u53EF\u6062\u590D\uFF09",
|
|
277
280
|
sessionResultWorkspaceRecycledRefused: "\u5DF2\u628A\u8BE5\u5DE5\u4F5C\u533A {count} \u4E2A\u4F1A\u8BDD\u79FB\u5165\u56DE\u6536\u7AD9\uFF0C{size}\uFF08\u53EF\u6062\u590D\uFF1B{refused} \u4E2A\u88AB\u62D2\u7EDD\uFF09",
|
|
278
281
|
sessionResultRestored: "\u5DF2\u8FD8\u539F\u8BE5\u4F1A\u8BDD",
|
|
282
|
+
sessionResultRestoredWithSubsessions: "\u5DF2\u8FD8\u539F\u8BE5\u4F1A\u8BDD\uFF0C\u5E76\u8FDE\u5E26\u8FD8\u539F {count} \u4E2A\u5B50\u4F1A\u8BDD",
|
|
283
|
+
sessionResultRestoredPartial: "\u5DF2\u8FD8\u539F\u8BE5\u4F1A\u8BDD\uFF0C\u53E6\u6709 {failed} \u4E2A\u5B50\u4F1A\u8BDD\u4ECD\u7559\u5728\u56DE\u6536\u7AD9",
|
|
284
|
+
sessionResultRestoredParentBinned: "\u5DF2\u8FD8\u539F\u8BE5\u4F1A\u8BDD\uFF08\u5176\u7236\u4F1A\u8BDD\u4ECD\u5728\u56DE\u6536\u7AD9\uFF09",
|
|
279
285
|
sessionResultPurged: "\u5DF2\u6C38\u4E45\u6E05\u9664\u8BE5\u6761\u76EE",
|
|
280
286
|
sessionResultSweepNone: "\u6CA1\u6709\u9700\u8981\u6E05\u7406\u7684\u6761\u76EE",
|
|
281
287
|
sessionResultSweepDone: "\u5DF2\u6E05\u7406 {count} \u4E2A\u6761\u76EE",
|
|
@@ -329,6 +335,8 @@ var OVER_SSH_DICTS = {
|
|
|
329
335
|
sessionPanelSessions: "Sessions",
|
|
330
336
|
sessionPanelBin: "Recycle bin",
|
|
331
337
|
sessionPanelBinEmpty: "The recycle bin is empty.",
|
|
338
|
+
sessionPanelBinUnreadable: "{count} more bin entr(y/ies) cannot be shown (no session id); restart the host or check the bin index.",
|
|
339
|
+
sessionPanelBinRowUnaddressable: "This entry has no id, so it cannot be restored or purged",
|
|
332
340
|
sessionPanelSelectAll: "Select deletable",
|
|
333
341
|
sessionPanelClear: "Clear selection",
|
|
334
342
|
sessionPanelDeleteRecycle: "Delete to bin",
|
|
@@ -401,6 +409,7 @@ var OVER_SSH_DICTS = {
|
|
|
401
409
|
rootsErrorIdRequired: "Enter a connection id.",
|
|
402
410
|
rootsErrorIdInvalid: "A connection id may contain letters, digits, dots, underscores and dashes, and must start with a letter or a digit.",
|
|
403
411
|
rootsErrorIdDuplicate: "That connection id already exists.",
|
|
412
|
+
rootsErrorConnectionMissing: "That connection has been deleted; close the editor and refresh.",
|
|
404
413
|
rootsErrorHostRequired: "Enter a host name.",
|
|
405
414
|
rootsErrorUserRequired: "Enter a user name.",
|
|
406
415
|
rootsErrorPort: "The port must be an integer between 1 and 65535.",
|
|
@@ -518,6 +527,9 @@ var OVER_SSH_DICTS = {
|
|
|
518
527
|
sessionResultWorkspaceRecycled: "Moved {count} sessions of that workspace to the recycle bin, {size} (recoverable)",
|
|
519
528
|
sessionResultWorkspaceRecycledRefused: "Moved {count} sessions of that workspace to the recycle bin, {size} (recoverable; {refused} refused)",
|
|
520
529
|
sessionResultRestored: "Restored that session",
|
|
530
|
+
sessionResultRestoredWithSubsessions: "Restored that session and {count} sub-session(s)",
|
|
531
|
+
sessionResultRestoredPartial: "Restored that session; {failed} sub-session(s) stayed in the bin",
|
|
532
|
+
sessionResultRestoredParentBinned: "Restored that session (its parent is still in the bin)",
|
|
521
533
|
sessionResultPurged: "Permanently removed that entry",
|
|
522
534
|
sessionResultSweepNone: "Nothing to sweep",
|
|
523
535
|
sessionResultSweepDone: "Swept {count} entries",
|
|
@@ -1438,15 +1450,29 @@ function createCardPortal(options) {
|
|
|
1438
1450
|
};
|
|
1439
1451
|
}
|
|
1440
1452
|
function focusFirstControl(container) {
|
|
1453
|
+
const all = container?.querySelectorAll?.(CARD_FOCUS_SELECTOR);
|
|
1454
|
+
if (all !== void 0 && all !== null) {
|
|
1455
|
+
for (const candidate of Array.from(all)) {
|
|
1456
|
+
if (!isVisibleControl(candidate)) continue;
|
|
1457
|
+
candidate.focus?.();
|
|
1458
|
+
return true;
|
|
1459
|
+
}
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1441
1462
|
const target = container?.querySelector?.(CARD_FOCUS_SELECTOR) ?? void 0;
|
|
1442
1463
|
if (target === void 0 || typeof target.focus !== "function") return false;
|
|
1443
1464
|
target.focus();
|
|
1444
1465
|
return true;
|
|
1445
1466
|
}
|
|
1467
|
+
function isVisibleControl(element) {
|
|
1468
|
+
const measurable = element.getClientRects;
|
|
1469
|
+
if (typeof measurable === "function") return measurable.call(element).length > 0;
|
|
1470
|
+
return element.hidden !== true;
|
|
1471
|
+
}
|
|
1446
1472
|
function focusableWithin(container) {
|
|
1447
1473
|
const found = container?.querySelectorAll?.(CARD_FOCUS_SELECTOR);
|
|
1448
1474
|
if (found === null || found === void 0) return [];
|
|
1449
|
-
return Array.from(found);
|
|
1475
|
+
return Array.from(found).filter(isVisibleControl);
|
|
1450
1476
|
}
|
|
1451
1477
|
function trapTab(event, container, active) {
|
|
1452
1478
|
const focusables = focusableWithin(container);
|
|
@@ -1698,8 +1724,8 @@ function parseRootsSnapshot(value) {
|
|
|
1698
1724
|
const row = toRootRow(entry, "config", false);
|
|
1699
1725
|
const identity = row === void 0 ? void 0 : rootKeyIdentity(row.key);
|
|
1700
1726
|
if (row !== void 0 && identity !== void 0 && !seen.has(identity)) {
|
|
1701
|
-
const scoped = userWorkspaces.get(identity);
|
|
1702
|
-
const scopedSessions = userSessions.get(identity);
|
|
1727
|
+
const scoped = row.workspaces ?? userWorkspaces.get(identity);
|
|
1728
|
+
const scopedSessions = row.sessions ?? userSessions.get(identity);
|
|
1703
1729
|
roots.push({
|
|
1704
1730
|
...row,
|
|
1705
1731
|
inUserFile: userKeys.has(identity),
|
|
@@ -1723,7 +1749,14 @@ function parseRootsSnapshot(value) {
|
|
|
1723
1749
|
}
|
|
1724
1750
|
const warnings = (Array.isArray(payload.warnings) ? payload.warnings : []).filter((item) => typeof item === "string");
|
|
1725
1751
|
const anchorRoot = readString(payload, "anchorRoot");
|
|
1726
|
-
|
|
1752
|
+
const scopedForSession = readString(payload, "scopedForSession");
|
|
1753
|
+
return {
|
|
1754
|
+
roots,
|
|
1755
|
+
connections,
|
|
1756
|
+
warnings,
|
|
1757
|
+
...anchorRoot === void 0 ? {} : { anchorRoot },
|
|
1758
|
+
...scopedForSession === void 0 ? {} : { scopedForSession }
|
|
1759
|
+
};
|
|
1727
1760
|
}
|
|
1728
1761
|
function isAbsoluteRemotePath(path) {
|
|
1729
1762
|
if (!path.startsWith("/")) return false;
|
|
@@ -1912,13 +1945,27 @@ async function requestSnapshot(fetchImpl, path, init) {
|
|
|
1912
1945
|
const reason = typeof body?.error === "string" ? body.error : transportFailureReason(response.status, path);
|
|
1913
1946
|
return { ok: false, error: reason };
|
|
1914
1947
|
}
|
|
1948
|
+
if (typeof body === "object" && body !== null && body["ok"] === false && typeof body["error"] === "string") {
|
|
1949
|
+
return { ok: false, error: body["error"] };
|
|
1950
|
+
}
|
|
1915
1951
|
return { ok: true, value: parseRootsSnapshot(body) };
|
|
1916
1952
|
} catch (error) {
|
|
1917
1953
|
return { ok: false, error: error instanceof Error ? error.name : "network" };
|
|
1918
1954
|
}
|
|
1919
1955
|
}
|
|
1920
1956
|
function createRootsApi(fetchImpl, path = ROOTS_API_PATH) {
|
|
1921
|
-
const load = async (
|
|
1957
|
+
const load = async (options = {}) => {
|
|
1958
|
+
const normalized = options instanceof AbortSignal ? { signal: options } : options;
|
|
1959
|
+
const query = new URLSearchParams();
|
|
1960
|
+
if (normalized.session !== void 0 && normalized.session !== "") query.set("session", normalized.session);
|
|
1961
|
+
if (normalized.workspace !== void 0 && normalized.workspace !== "") query.set("workspace", normalized.workspace);
|
|
1962
|
+
const suffix = query.size === 0 ? "" : `?${query.toString()}`;
|
|
1963
|
+
return requestSnapshot(fetchImpl, `${path}${suffix}`, {
|
|
1964
|
+
method: "GET",
|
|
1965
|
+
headers: { accept: "application/json" },
|
|
1966
|
+
...normalized.signal === void 0 ? {} : { signal: normalized.signal }
|
|
1967
|
+
});
|
|
1968
|
+
};
|
|
1922
1969
|
const send = async (payload) => requestSnapshot(fetchImpl, path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) });
|
|
1923
1970
|
const sendRaw = async (payload) => {
|
|
1924
1971
|
try {
|
|
@@ -2435,7 +2482,16 @@ function createTransferProgress(options) {
|
|
|
2435
2482
|
setState(transferPanelPollStarted(state, showLoading));
|
|
2436
2483
|
const options_ = mode === "park" ? { waitMs, signal: controller.signal } : { signal: controller.signal };
|
|
2437
2484
|
issued += 1;
|
|
2438
|
-
|
|
2485
|
+
let result;
|
|
2486
|
+
try {
|
|
2487
|
+
result = await options.io.list(options_);
|
|
2488
|
+
} catch (error) {
|
|
2489
|
+
if (controller.signal.aborted || disposed) {
|
|
2490
|
+
if (inFlight === controller) inFlight = void 0;
|
|
2491
|
+
continue;
|
|
2492
|
+
}
|
|
2493
|
+
result = { ok: false, error: error instanceof Error ? error.message : "list-failed" };
|
|
2494
|
+
}
|
|
2439
2495
|
if (inFlight === controller) inFlight = void 0;
|
|
2440
2496
|
if (disposed) return;
|
|
2441
2497
|
if (mine !== generation) {
|
|
@@ -2476,6 +2532,7 @@ function createTransferProgress(options) {
|
|
|
2476
2532
|
},
|
|
2477
2533
|
clear: () => {
|
|
2478
2534
|
if (disposed) return;
|
|
2535
|
+
if (state.clearing) return;
|
|
2479
2536
|
setState(transferPanelClearing(state));
|
|
2480
2537
|
void options.io.clear().then((result) => {
|
|
2481
2538
|
if (disposed) return;
|
|
@@ -2574,7 +2631,9 @@ var ROOTS_EDIT_BACKDROP_CLASS = classes(CLASS.backdrop, CLASS.backdropSettings);
|
|
|
2574
2631
|
function createRootsPanelController(options) {
|
|
2575
2632
|
let state = initialRootsPanelState();
|
|
2576
2633
|
const listeners = /* @__PURE__ */ new Set();
|
|
2577
|
-
let
|
|
2634
|
+
let readToken = 0;
|
|
2635
|
+
let writeToken = 0;
|
|
2636
|
+
let epoch = 0;
|
|
2578
2637
|
let disposed = false;
|
|
2579
2638
|
const requests = createAbortGate();
|
|
2580
2639
|
const dispatch = (action) => {
|
|
@@ -2583,22 +2642,30 @@ function createRootsPanelController(options) {
|
|
|
2583
2642
|
state = next;
|
|
2584
2643
|
for (const listener of [...listeners]) listener();
|
|
2585
2644
|
};
|
|
2586
|
-
const
|
|
2645
|
+
const latestRead = (mine, issuedEpoch) => !disposed && mine === readToken && issuedEpoch === epoch;
|
|
2646
|
+
const latestWrite = (mine) => !disposed && mine === writeToken;
|
|
2587
2647
|
const reload = async () => {
|
|
2588
|
-
const mine = ++
|
|
2648
|
+
const mine = ++readToken;
|
|
2649
|
+
const issuedEpoch = epoch;
|
|
2589
2650
|
const signal = requests.begin();
|
|
2590
2651
|
dispatch({ type: "load" });
|
|
2591
2652
|
const result = await options.api.load(signal);
|
|
2592
|
-
if (!
|
|
2653
|
+
if (!latestRead(mine, issuedEpoch)) return;
|
|
2654
|
+
if (result.ok && result.value.warnings.includes("roots-invalid-answer")) {
|
|
2655
|
+
dispatch({ type: "failed", error: "roots-invalid-answer" });
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2593
2658
|
if (result.ok) dispatch({ type: "loaded", snapshot: result.value });
|
|
2594
2659
|
else dispatch({ type: "failed", error: result.error });
|
|
2595
2660
|
};
|
|
2596
2661
|
const settle = async (mine, scope, result, resetDrafts) => {
|
|
2597
|
-
if (!
|
|
2662
|
+
if (!latestWrite(mine)) return;
|
|
2598
2663
|
if (!result.ok) {
|
|
2599
2664
|
dispatch({ type: "failed", error: result.error, scope });
|
|
2600
2665
|
return;
|
|
2601
2666
|
}
|
|
2667
|
+
epoch += 1;
|
|
2668
|
+
void result.value;
|
|
2602
2669
|
dispatch({ type: "succeeded", scope, ...resetDrafts ? { resetDrafts: true } : {} });
|
|
2603
2670
|
await reload();
|
|
2604
2671
|
};
|
|
@@ -2611,7 +2678,7 @@ function createRootsPanelController(options) {
|
|
|
2611
2678
|
dispatch({ type: "refused", scope: "roots", message: validated.message });
|
|
2612
2679
|
return;
|
|
2613
2680
|
}
|
|
2614
|
-
const mine = ++
|
|
2681
|
+
const mine = ++writeToken;
|
|
2615
2682
|
dispatch({ type: "submitted" });
|
|
2616
2683
|
const result = await options.api.send({ action: "add-root", ...validated.value });
|
|
2617
2684
|
await settle(mine, "roots", result, true);
|
|
@@ -2637,7 +2704,7 @@ function createRootsPanelController(options) {
|
|
|
2637
2704
|
dispatch({ type: "refused", scope: "connection", message: validated.message });
|
|
2638
2705
|
return;
|
|
2639
2706
|
}
|
|
2640
|
-
const mine = ++
|
|
2707
|
+
const mine = ++writeToken;
|
|
2641
2708
|
dispatch({ type: "submitted" });
|
|
2642
2709
|
const result = await options.api.send({ action: "add-connection", ...validated.value });
|
|
2643
2710
|
await settle(mine, "connection", result, true);
|
|
@@ -2646,7 +2713,7 @@ function createRootsPanelController(options) {
|
|
|
2646
2713
|
const row = selectRootsView(state).rows.find((candidate) => candidate.key === key);
|
|
2647
2714
|
if (row === void 0 || !row.removable) return;
|
|
2648
2715
|
if (!canEdit(state)) return;
|
|
2649
|
-
const mine = ++
|
|
2716
|
+
const mine = ++writeToken;
|
|
2650
2717
|
dispatch({ type: "remove", key });
|
|
2651
2718
|
const result = await options.api.send({ action: "remove-root", key });
|
|
2652
2719
|
await settle(mine, "roots", result, false);
|
|
@@ -2685,6 +2752,11 @@ function createRootsPanelController(options) {
|
|
|
2685
2752
|
const editingId = state.editingId;
|
|
2686
2753
|
if (editingId === void 0) return;
|
|
2687
2754
|
if (!canEdit(state)) return;
|
|
2755
|
+
if (!(state.snapshot?.connections ?? []).some((row) => row.id === editingId)) {
|
|
2756
|
+
dispatch({ type: "edit-ended" });
|
|
2757
|
+
dispatch({ type: "refused", scope: "edit", message: { key: "rootsErrorConnectionMissing" } });
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2688
2760
|
const editDrafts = state.editDrafts;
|
|
2689
2761
|
const portText = editDrafts.port.trim();
|
|
2690
2762
|
const port = portText === "" ? void 0 : Number(portText);
|
|
@@ -2702,7 +2774,7 @@ function createRootsPanelController(options) {
|
|
|
2702
2774
|
dispatch({ type: "refused", scope: "edit", message: validated.message });
|
|
2703
2775
|
return;
|
|
2704
2776
|
}
|
|
2705
|
-
const mine = ++
|
|
2777
|
+
const mine = ++writeToken;
|
|
2706
2778
|
dispatch({ type: "submitted" });
|
|
2707
2779
|
const result = await options.api.send({ action: "update-connection", id: editingId, ...validated.value });
|
|
2708
2780
|
await settle(mine, "edit", result, true);
|
|
@@ -2716,16 +2788,16 @@ function createRootsPanelController(options) {
|
|
|
2716
2788
|
const confirmRemoveConnection = async (id) => {
|
|
2717
2789
|
if (state.confirmingRemove !== id) return;
|
|
2718
2790
|
if (!canEdit(state)) return;
|
|
2719
|
-
const mine = ++
|
|
2791
|
+
const mine = ++writeToken;
|
|
2720
2792
|
dispatch({ type: "submitted" });
|
|
2721
2793
|
const result = await options.api.send({ action: "remove-connection", id });
|
|
2722
2794
|
await settle(mine, "edit", result, true);
|
|
2723
2795
|
};
|
|
2724
2796
|
const loadImport = async () => {
|
|
2725
|
-
const mine = ++
|
|
2797
|
+
const mine = ++readToken;
|
|
2726
2798
|
dispatch({ type: "import-busy" });
|
|
2727
2799
|
const result = await options.api.sendRaw({ action: "list-ssh-config" });
|
|
2728
|
-
if (
|
|
2800
|
+
if (disposed || mine !== readToken) return;
|
|
2729
2801
|
if (!result.ok) {
|
|
2730
2802
|
dispatch({ type: "import-failed", error: result.error });
|
|
2731
2803
|
return;
|
|
@@ -2739,10 +2811,10 @@ function createRootsPanelController(options) {
|
|
|
2739
2811
|
const submitImport = async () => {
|
|
2740
2812
|
const selected = state.importSelected ?? [];
|
|
2741
2813
|
if (selected.length === 0) return;
|
|
2742
|
-
const mine = ++
|
|
2814
|
+
const mine = ++writeToken;
|
|
2743
2815
|
dispatch({ type: "import-busy" });
|
|
2744
2816
|
const result = await options.api.sendRaw({ action: "import-ssh-config", aliases: selected });
|
|
2745
|
-
if (!
|
|
2817
|
+
if (!latestWrite(mine)) return;
|
|
2746
2818
|
if (!result.ok) {
|
|
2747
2819
|
dispatch({ type: "import-failed", error: result.error });
|
|
2748
2820
|
return;
|
|
@@ -3625,10 +3697,10 @@ function buildPanelModel(copy, options = {}) {
|
|
|
3625
3697
|
// src/shared/remote-identity.ts
|
|
3626
3698
|
var SSH_DEV_REMOTE_NAMESPACE = "sshdev";
|
|
3627
3699
|
var SSH_DEV_REMOTE_SERVICE = "sshDevRemote";
|
|
3628
|
-
|
|
3629
|
-
// src/client/core/remote.ts
|
|
3630
3700
|
var SSH_DEV_REMOTE_METHOD = "connections";
|
|
3631
3701
|
var SSH_DEV_REMOTE_PACKAGE = "dsh-over-ssh";
|
|
3702
|
+
|
|
3703
|
+
// src/client/core/remote.ts
|
|
3632
3704
|
var SSH_DEV_CONNECTIONS_DESCRIPTOR = {
|
|
3633
3705
|
id: `${SSH_DEV_REMOTE_PACKAGE}#${SSH_DEV_REMOTE_NAMESPACE}/${SSH_DEV_REMOTE_METHOD}`,
|
|
3634
3706
|
service: SSH_DEV_REMOTE_SERVICE,
|
|
@@ -3828,10 +3900,10 @@ function toSessionRowView(raw) {
|
|
|
3828
3900
|
};
|
|
3829
3901
|
}
|
|
3830
3902
|
function toBinRowView(raw) {
|
|
3831
|
-
const
|
|
3903
|
+
const rawEntryId = raw["entryId"];
|
|
3832
3904
|
const sessionId = raw["sessionId"];
|
|
3833
|
-
if (typeof entryId !== "string" || entryId === "") return void 0;
|
|
3834
3905
|
if (typeof sessionId !== "string" || sessionId === "") return void 0;
|
|
3906
|
+
const entryId = typeof rawEntryId === "string" ? rawEntryId : "";
|
|
3835
3907
|
return {
|
|
3836
3908
|
entryId,
|
|
3837
3909
|
sessionId,
|
|
@@ -3846,6 +3918,8 @@ function emptySnapshot() {
|
|
|
3846
3918
|
available: false,
|
|
3847
3919
|
sessions: [],
|
|
3848
3920
|
bin: [],
|
|
3921
|
+
binUnaddressable: 0,
|
|
3922
|
+
binUnreadable: 0,
|
|
3849
3923
|
policy: { maxEntries: 0, maxBytes: 0, maxAgeMs: 0 },
|
|
3850
3924
|
totals: { sessions: 0, bytes: 0, subsessions: 0, live: 0, running: 0 }
|
|
3851
3925
|
};
|
|
@@ -3917,13 +3991,28 @@ function parseSessionSnapshot(value) {
|
|
|
3917
3991
|
const available = body["available"] === true;
|
|
3918
3992
|
const sessions = Array.isArray(body["sessions"]) ? body["sessions"].flatMap((raw) => typeof raw === "object" && raw !== null ? [toSessionRowView(raw)].filter((row) => row !== void 0) : []) : [];
|
|
3919
3993
|
const unblocked = unblockOrphanSubsessions(sessions);
|
|
3920
|
-
|
|
3994
|
+
let binUnreadable = 0;
|
|
3995
|
+
const bin = Array.isArray(body["bin"]) ? body["bin"].flatMap((raw) => {
|
|
3996
|
+
if (typeof raw !== "object" || raw === null) {
|
|
3997
|
+
binUnreadable += 1;
|
|
3998
|
+
return [];
|
|
3999
|
+
}
|
|
4000
|
+
const row = toBinRowView(raw);
|
|
4001
|
+
if (row === void 0) {
|
|
4002
|
+
binUnreadable += 1;
|
|
4003
|
+
return [];
|
|
4004
|
+
}
|
|
4005
|
+
return [row];
|
|
4006
|
+
}) : [];
|
|
4007
|
+
const binUnaddressable = bin.filter((row) => row.entryId === "").length;
|
|
3921
4008
|
const rawPolicy = typeof body["policy"] === "object" && body["policy"] !== null ? body["policy"] : {};
|
|
3922
4009
|
const numberOr = (key) => typeof rawPolicy[key] === "number" && Number.isFinite(rawPolicy[key]) ? rawPolicy[key] : 0;
|
|
3923
4010
|
return {
|
|
3924
4011
|
available,
|
|
3925
4012
|
sessions: unblocked,
|
|
3926
4013
|
bin,
|
|
4014
|
+
binUnaddressable,
|
|
4015
|
+
binUnreadable,
|
|
3927
4016
|
policy: { maxEntries: numberOr("maxEntries"), maxBytes: numberOr("maxBytes"), maxAgeMs: numberOr("maxAgeMs") },
|
|
3928
4017
|
totals: {
|
|
3929
4018
|
sessions: unblocked.length,
|
|
@@ -4008,7 +4097,14 @@ async function deleteWorkspaceSessions(io, cwd, options = {}) {
|
|
|
4008
4097
|
}
|
|
4009
4098
|
async function restoreSession(io, entryId) {
|
|
4010
4099
|
if (entryId === "") return { kind: "error", message: { key: "sessionResultMissingEntryId" } };
|
|
4011
|
-
return postAction(io, { action: "restore", entryId }, () =>
|
|
4100
|
+
return postAction(io, { action: "restore", entryId }, (value) => {
|
|
4101
|
+
const failed = typeof value["failedSubsessions"] === "number" ? value["failedSubsessions"] : 0;
|
|
4102
|
+
const restored = typeof value["restoredSubsessions"] === "number" ? value["restoredSubsessions"] : 0;
|
|
4103
|
+
if (failed > 0) return { key: "sessionResultRestoredPartial", params: { failed } };
|
|
4104
|
+
if (restored > 0) return { key: "sessionResultRestoredWithSubsessions", params: { count: restored } };
|
|
4105
|
+
if (value["parentStillBinned"] === true) return { key: "sessionResultRestoredParentBinned" };
|
|
4106
|
+
return { key: "sessionResultRestored" };
|
|
4107
|
+
});
|
|
4012
4108
|
}
|
|
4013
4109
|
async function purgeSession(io, entryId) {
|
|
4014
4110
|
if (entryId === "") return { kind: "error", message: { key: "sessionResultMissingEntryId" } };
|
|
@@ -4117,9 +4213,10 @@ var SessionRow = (0, import_react7.memo)(function SessionRow2(props) {
|
|
|
4117
4213
|
});
|
|
4118
4214
|
function renderBinRow(props) {
|
|
4119
4215
|
const { row, copy, busy } = props;
|
|
4216
|
+
const unaddressable = row.entryId === "";
|
|
4120
4217
|
return (0, import_react7.createElement)(
|
|
4121
4218
|
"li",
|
|
4122
|
-
{ key: row.entryId },
|
|
4219
|
+
{ key: unaddressable ? `unaddressable:${row.sessionId}` : row.entryId },
|
|
4123
4220
|
(0, import_react7.createElement)(
|
|
4124
4221
|
"div",
|
|
4125
4222
|
{ className: CLASS.row },
|
|
@@ -4131,20 +4228,22 @@ function renderBinRow(props) {
|
|
|
4131
4228
|
(0, import_react7.createElement)(
|
|
4132
4229
|
"span",
|
|
4133
4230
|
{ className: CLASS.rowActions },
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4231
|
+
...unaddressable ? [(0, import_react7.createElement)("span", { className: CLASS.rowMeta, title: copy.binRowUnaddressable }, copy.binRowUnaddressable)] : [
|
|
4232
|
+
Button({
|
|
4233
|
+
variant: "secondary",
|
|
4234
|
+
size: "compact",
|
|
4235
|
+
disabled: busy,
|
|
4236
|
+
onClick: () => props.onRestore(row.entryId),
|
|
4237
|
+
children: copy.restore
|
|
4238
|
+
}),
|
|
4239
|
+
Button({
|
|
4240
|
+
variant: "ghost",
|
|
4241
|
+
size: "compact",
|
|
4242
|
+
disabled: busy,
|
|
4243
|
+
onClick: () => props.onPurge(row.entryId),
|
|
4244
|
+
children: copy.purge
|
|
4245
|
+
})
|
|
4246
|
+
]
|
|
4148
4247
|
)
|
|
4149
4248
|
)
|
|
4150
4249
|
);
|
|
@@ -4422,10 +4521,10 @@ function SessionPanel({ copy, io, close }) {
|
|
|
4422
4521
|
(0, import_react7.createElement)("h3", { className: CLASS.blockTitle }, copy.binHeading),
|
|
4423
4522
|
// Same rule as the inventory: 「回收站为空」 is a claim about an **answer**. While nothing has been read (or the
|
|
4424
4523
|
// read failed) the status line above says what is happening (audit 2026-11-01b ui P1-3).
|
|
4425
|
-
phase.kind !== "ready" ? null : snapshot.bin.length === 0 ? (0, import_react7.createElement)("p", { className: CLASS.empty }, copy.binEmpty) : (0, import_react7.createElement)(
|
|
4524
|
+
phase.kind !== "ready" ? null : snapshot.bin.length === 0 && snapshot.binUnreadable === 0 ? (0, import_react7.createElement)("p", { className: CLASS.empty }, copy.binEmpty) : (0, import_react7.createElement)(
|
|
4426
4525
|
"div",
|
|
4427
4526
|
{ className: CLASS.block },
|
|
4428
|
-
(0, import_react7.createElement)(
|
|
4527
|
+
snapshot.bin.length === 0 ? null : (0, import_react7.createElement)(
|
|
4429
4528
|
"ul",
|
|
4430
4529
|
{ className: CLASS.list },
|
|
4431
4530
|
...snapshot.bin.map(
|
|
@@ -4438,6 +4537,7 @@ function SessionPanel({ copy, io, close }) {
|
|
|
4438
4537
|
})
|
|
4439
4538
|
)
|
|
4440
4539
|
),
|
|
4540
|
+
snapshot.binUnreadable > 0 ? (0, import_react7.createElement)("p", { className: CLASS.empty }, copy.binUnreadable(snapshot.binUnreadable)) : null,
|
|
4441
4541
|
Button({
|
|
4442
4542
|
variant: "secondary",
|
|
4443
4543
|
size: "compact",
|
|
@@ -4526,6 +4626,7 @@ function createSessionRowActionStore(deps) {
|
|
|
4526
4626
|
};
|
|
4527
4627
|
});
|
|
4528
4628
|
let cancelAutoClose;
|
|
4629
|
+
let postInFlight = false;
|
|
4529
4630
|
const stopAutoClose = () => {
|
|
4530
4631
|
cancelAutoClose?.();
|
|
4531
4632
|
cancelAutoClose = void 0;
|
|
@@ -4550,18 +4651,21 @@ function createSessionRowActionStore(deps) {
|
|
|
4550
4651
|
const confirm = async () => {
|
|
4551
4652
|
const request = state.request;
|
|
4552
4653
|
const row = state.row;
|
|
4553
|
-
if (disposed || request === void 0 || state.busy) return;
|
|
4654
|
+
if (disposed || request === void 0 || state.busy || postInFlight) return;
|
|
4554
4655
|
if (state.phase !== "ready" || row === void 0 || !row.deletable) return;
|
|
4555
4656
|
const stillPending = () => {
|
|
4556
4657
|
const current = state.request;
|
|
4557
4658
|
return current !== void 0 && current.kind === request.kind && current.sessionId === request.sessionId;
|
|
4558
4659
|
};
|
|
4660
|
+
postInFlight = true;
|
|
4559
4661
|
dispatch({ ...state, busy: true, outcome: void 0 });
|
|
4560
4662
|
let result;
|
|
4561
4663
|
try {
|
|
4562
4664
|
result = await deleteSessions(deps.io, [request.sessionId], request.kind === "delete" ? { permanent: true } : {});
|
|
4563
4665
|
} catch {
|
|
4564
4666
|
result = { kind: "error", message: { key: "sessionResultActionFailed" } };
|
|
4667
|
+
} finally {
|
|
4668
|
+
postInFlight = false;
|
|
4565
4669
|
}
|
|
4566
4670
|
if (disposed || !stillPending()) return;
|
|
4567
4671
|
dispatch({ ...state, busy: false, outcome: result });
|
|
@@ -4584,7 +4688,7 @@ function createSessionRowActionStore(deps) {
|
|
|
4584
4688
|
request: (request) => {
|
|
4585
4689
|
if (disposed) return;
|
|
4586
4690
|
stopAutoClose();
|
|
4587
|
-
dispatch({ request, phase: "loading", row: void 0, busy:
|
|
4691
|
+
dispatch({ request, phase: "loading", row: void 0, busy: postInFlight, outcome: void 0 });
|
|
4588
4692
|
},
|
|
4589
4693
|
load,
|
|
4590
4694
|
confirm,
|
|
@@ -5281,7 +5385,7 @@ function DirectoryBrowserSection({
|
|
|
5281
5385
|
listRef
|
|
5282
5386
|
}) {
|
|
5283
5387
|
const view = selectDirectoryBrowserView(state);
|
|
5284
|
-
const gate = disabled;
|
|
5388
|
+
const gate = disabled || state.pending;
|
|
5285
5389
|
const connectionFieldId = `${idPrefix}-connection`;
|
|
5286
5390
|
const modeButton = (mode, label) => Button({
|
|
5287
5391
|
// The active world is the brand-filled one: the switch reads without an extra attribute, and both
|
|
@@ -5503,12 +5607,18 @@ function createSessionDirectoryIo(options) {
|
|
|
5503
5607
|
});
|
|
5504
5608
|
const rootsApi = createRootsApi(options.fetchImpl, path);
|
|
5505
5609
|
const loadRoots = async (workspacePath, sessionId) => {
|
|
5506
|
-
const result = await rootsApi.load(
|
|
5610
|
+
const result = await rootsApi.load({
|
|
5611
|
+
...sessionId === void 0 || sessionId === "" ? {} : { session: sessionId },
|
|
5612
|
+
...workspacePath === void 0 || workspacePath === "" ? {} : { workspace: workspacePath }
|
|
5613
|
+
});
|
|
5507
5614
|
if (!result.ok) return result;
|
|
5508
5615
|
if (result.value.warnings.includes("roots-invalid-answer")) {
|
|
5509
5616
|
return { ok: false, error: "roots-invalid-answer" };
|
|
5510
5617
|
}
|
|
5511
5618
|
const all = result.value.roots;
|
|
5619
|
+
if (sessionId !== void 0 && sessionId !== "" && result.value.scopedForSession === sessionId) {
|
|
5620
|
+
return { ok: true, value: all };
|
|
5621
|
+
}
|
|
5512
5622
|
if (workspacePath === void 0 || workspacePath === "") return { ok: true, value: [] };
|
|
5513
5623
|
return {
|
|
5514
5624
|
ok: true,
|
|
@@ -5651,7 +5761,11 @@ function selectSessionDirectoryView(state, copy) {
|
|
|
5651
5761
|
title: row.label ?? (row.location === "remote" ? `${row.connectionId ?? ""}:${row.path}` : row.path),
|
|
5652
5762
|
subtitle: row.label === void 0 ? "" : row.path,
|
|
5653
5763
|
badge: row.location === "remote" ? copy["rootsBadgeRemote"] : copy["rootsBadgeLocal"],
|
|
5654
|
-
|
|
5764
|
+
// fork 继承来的行没有删除按钮:它在父亲那里才是"本会话添加的",在这里点删除会把父会话的
|
|
5765
|
+
// 目录拿走(宿主也会拒绝)。没有 sessions 字段的历史条目按文件权限判定,保持旧行为。
|
|
5766
|
+
removable: row.inUserFile && // 会话型条目只有**本会话**能删;`state.sessionId` 还没到也不能放行(那会把父会话的目录
|
|
5767
|
+
// 交给 fork 去删,宿主会拒,用户看到的是一次"按了没反应")。历史条目(无 sessions)按文件权限。
|
|
5768
|
+
(row.sessions === void 0 || state.sessionId !== void 0 && row.sessions.includes(state.sessionId))
|
|
5655
5769
|
}))
|
|
5656
5770
|
],
|
|
5657
5771
|
rootsPending: state.rootsPending,
|
|
@@ -5706,11 +5820,14 @@ function createSessionDirectory(deps) {
|
|
|
5706
5820
|
const commit = async (path, connectionId) => {
|
|
5707
5821
|
if (disposed || state.pending) return;
|
|
5708
5822
|
const location = connectionId === void 0 ? "local" : "remote";
|
|
5709
|
-
const validated = validateRootDraft(
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5823
|
+
const validated = validateRootDraft(
|
|
5824
|
+
{
|
|
5825
|
+
location,
|
|
5826
|
+
path,
|
|
5827
|
+
...connectionId === void 0 ? {} : { connectionId }
|
|
5828
|
+
},
|
|
5829
|
+
state.roots
|
|
5830
|
+
);
|
|
5714
5831
|
if (!validated.ok) {
|
|
5715
5832
|
dispatch({ type: "refused", message: validated.message });
|
|
5716
5833
|
return;
|
|
@@ -5819,7 +5936,10 @@ function SessionDirectorySection({
|
|
|
5819
5936
|
{ className: CLASS.block },
|
|
5820
5937
|
(0, import_react11.createElement)("h4", { className: CLASS.blockTitle }, copy.sessionRootsTitle),
|
|
5821
5938
|
(0, import_react11.createElement)("p", { className: CLASS.hint }, copy.sessionRootsHint),
|
|
5822
|
-
|
|
5939
|
+
// **有行就画列表**:工作区那一行是合成行,它不属于"已添加目录",但它是这张卡片最重要的一行。
|
|
5940
|
+
// 旧条件按"有没有已添加目录"决定画不画整张列表,结果是新会话只看到「还没有添加目录」,连会话
|
|
5941
|
+
// 自己在哪个工作区都不显示,必须先去添加一个目录才冒出来(审计 2026-11-06,用户截图)。
|
|
5942
|
+
view.roots.length === 0 ? view.rootsReading ? Notice({ role: "status", tone: "info", children: copy.sessionRootsLoading }) : (
|
|
5823
5943
|
// A failed read is not an empty list: the error notice below is the whole truth there, and
|
|
5824
5944
|
// 「还没有添加目录」 next to it would contradict it (same rule as the import dialog, ui P2-2/P2-3).
|
|
5825
5945
|
view.notice === "" ? (0, import_react11.createElement)("p", { className: CLASS.empty }, copy.sessionRootsEmpty) : null
|
|
@@ -5839,7 +5959,9 @@ function SessionDirectorySection({
|
|
|
5839
5959
|
Button({
|
|
5840
5960
|
variant: "ghost",
|
|
5841
5961
|
size: "compact",
|
|
5842
|
-
|
|
5962
|
+
// 刷新在飞时也禁用:否则一次点击会叠加"删除 + 一次刷新"两次远端写
|
|
5963
|
+
// (审计 2026-11-06 客户端 P2)。
|
|
5964
|
+
disabled: view.rootsPending || view.rootsReading,
|
|
5843
5965
|
onClick: () => actions.onRemoveRoot(row.key),
|
|
5844
5966
|
children: copy.rootsRemove
|
|
5845
5967
|
})
|
|
@@ -5847,6 +5969,8 @@ function SessionDirectorySection({
|
|
|
5847
5969
|
)
|
|
5848
5970
|
)
|
|
5849
5971
|
),
|
|
5972
|
+
// 列表里只剩工作区行时,"还没有添加目录"仍然是有用的下一步提示;但它不再替换列表。
|
|
5973
|
+
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
5974
|
(0, import_react11.createElement)(
|
|
5851
5975
|
"div",
|
|
5852
5976
|
{ className: CLASS.actions },
|
|
@@ -5886,20 +6010,22 @@ function readWorkspacePath(props) {
|
|
|
5886
6010
|
const fromWorkspace = workspaces === void 0 ? void 0 : workspaces(
|
|
5887
6011
|
(snapshot) => sessionId === void 0 || sessionId === "" ? void 0 : snapshot?.items?.find((item) => item.sessionIds?.includes(sessionId) === true)?.path
|
|
5888
6012
|
);
|
|
5889
|
-
if (typeof fromSession === "string" && fromSession !== "") return fromSession;
|
|
5890
6013
|
if (typeof fromWorkspace === "string" && fromWorkspace !== "") return fromWorkspace;
|
|
6014
|
+
if (typeof fromSession === "string" && fromSession !== "") return fromSession;
|
|
5891
6015
|
return void 0;
|
|
5892
6016
|
}
|
|
5893
6017
|
function rootsQueryKey(state) {
|
|
5894
6018
|
if (!state.open) return "closed";
|
|
5895
|
-
|
|
5896
|
-
|
|
6019
|
+
const workspace = state.workspacePath === void 0 || state.workspacePath === "" ? "unknown" : state.workspacePath;
|
|
6020
|
+
const session = state.sessionId === void 0 || state.sessionId === "" ? "unknown" : state.sessionId;
|
|
6021
|
+
return `open:${workspace}\0${session}`;
|
|
5897
6022
|
}
|
|
5898
6023
|
function SessionDirectoryContainer(props) {
|
|
5899
6024
|
const { copy, io } = props;
|
|
5900
6025
|
const copyRef = (0, import_react11.useRef)(copy);
|
|
5901
6026
|
copyRef.current = copy;
|
|
5902
|
-
const
|
|
6027
|
+
const injected = props.controller;
|
|
6028
|
+
const [controller] = (0, import_react11.useState)(() => injected ?? createSessionDirectory({ io, copy: () => copyRef.current }));
|
|
5903
6029
|
const state = (0, import_react11.useSyncExternalStore)(controller.subscribe, controller.getState);
|
|
5904
6030
|
const workspacePath = readWorkspacePath(props);
|
|
5905
6031
|
const titleId = (0, import_react11.useId)();
|
|
@@ -5924,8 +6050,10 @@ function SessionDirectoryContainer(props) {
|
|
|
5924
6050
|
useCardTabTrap(state.open, portal.container);
|
|
5925
6051
|
(0, import_react11.useEffect)(() => {
|
|
5926
6052
|
controller.activate();
|
|
5927
|
-
return () =>
|
|
5928
|
-
|
|
6053
|
+
return () => {
|
|
6054
|
+
if (injected === void 0) controller.dispose();
|
|
6055
|
+
};
|
|
6056
|
+
}, [controller, injected]);
|
|
5929
6057
|
(0, import_react11.useLayoutEffect)(() => {
|
|
5930
6058
|
if (!state.open) return;
|
|
5931
6059
|
focusOrigin.current = captureFocusOrigin(browserPortalDocument());
|
|
@@ -6565,6 +6693,8 @@ function apply(ctx) {
|
|
|
6565
6693
|
sessionsHeading: dict.sessionPanelSessions,
|
|
6566
6694
|
binHeading: dict.sessionPanelBin,
|
|
6567
6695
|
binEmpty: dict.sessionPanelBinEmpty,
|
|
6696
|
+
binUnreadable: (count) => resolveMessage2(dict, { key: "sessionPanelBinUnreadable", params: { count } }),
|
|
6697
|
+
binRowUnaddressable: dict.sessionPanelBinRowUnaddressable,
|
|
6568
6698
|
selectAll: dict.sessionPanelSelectAll,
|
|
6569
6699
|
clearSelection: dict.sessionPanelClear,
|
|
6570
6700
|
deleteRecycle: dict.sessionPanelDeleteRecycle,
|
|
@@ -6889,6 +7019,7 @@ function apply(ctx) {
|
|
|
6889
7019
|
return await chooser.pickDirectory();
|
|
6890
7020
|
}
|
|
6891
7021
|
});
|
|
7022
|
+
const controller = createSessionDirectory({ io, copy: () => copy() });
|
|
6892
7023
|
const registerSessionEntry = () => ctx.slots.inject(
|
|
6893
7024
|
SESSION_DIRECTORY_SLOT,
|
|
6894
7025
|
() => ctx.slots.register(
|
|
@@ -6896,7 +7027,7 @@ function apply(ctx) {
|
|
|
6896
7027
|
name: SESSION_DIRECTORY_SLOT,
|
|
6897
7028
|
id: SESSION_DIRECTORY_ID,
|
|
6898
7029
|
order: SESSION_DIRECTORY_ORDER,
|
|
6899
|
-
inject: () => ({ copy: copy(), io })
|
|
7030
|
+
inject: () => ({ copy: copy(), io, controller })
|
|
6900
7031
|
},
|
|
6901
7032
|
SessionDirectoryContainer
|
|
6902
7033
|
)
|
|
@@ -6909,6 +7040,7 @@ function apply(ctx) {
|
|
|
6909
7040
|
return () => {
|
|
6910
7041
|
offLocaleChange();
|
|
6911
7042
|
disposeEntry();
|
|
7043
|
+
controller.dispose();
|
|
6912
7044
|
};
|
|
6913
7045
|
});
|
|
6914
7046
|
ctx.effect(() => {
|