dsh-over-ssh 0.2.2 → 0.2.4
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 +82 -4
- package/agents/agent-manifest.json +7 -7
- 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 +161 -60
- package/lib/file-references.js +136 -13
- package/lib/fs.js +2 -1
- package/lib/index.js +1245 -165
- package/lib/types/src/config.d.ts +44 -0
- package/lib/types/src/host/app/backend.d.ts +31 -0
- package/lib/types/src/host/app/routed-fs.d.ts +238 -0
- package/lib/types/src/host/app/session-admin/session-admin-contract.d.ts +17 -0
- package/lib/types/src/host/app/transport/transport-service.d.ts +66 -11
- package/lib/types/src/host/domain/capability.d.ts +6 -0
- package/lib/types/src/host/domain/roots/route-table.d.ts +101 -0
- package/lib/types/src/host/domain/session/session-admin.d.ts +8 -0
- package/lib/types/src/host/infra/backend/poll-watch.d.ts +119 -0
- package/lib/types/src/host/infra/backend/remote-backend.d.ts +297 -0
- package/lib/types/src/host/infra/cache/budget.d.ts +68 -0
- package/lib/types/src/host/infra/cache/versioned.d.ts +98 -0
- package/lib/types/src/host/infra/transport/ssh/askpass.d.ts +132 -0
- package/lib/types/src/host/infra/transport/transport.d.ts +48 -0
- package/lib/types/src/host/integration/agent/askpass-registry.d.ts +65 -0
- package/lib/types/src/host/integration/fs/remote-index.d.ts +183 -0
- package/lib/types/src/host/integration/roots/remote-home.d.ts +68 -0
- package/lib/types/src/host/integration/roots/root-registry.d.ts +148 -0
- package/lib/types/src/index.d.ts +7 -0
- package/lib/types/src/local-bridge.d.ts +4 -4
- package/lib/types/src/wiring/connections.d.ts +261 -0
- package/lib/types/src/wiring/install-memo.d.ts +31 -0
- 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,12 +82,16 @@ 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",
|
|
88
90
|
sessionPanelDeletePermanent: "\u6C38\u4E45\u5220\u9664\u2026",
|
|
89
91
|
sessionPanelRestore: "\u8FD8\u539F",
|
|
90
92
|
sessionPanelPurge: "\u6C38\u4E45\u6E05\u9664\u2026",
|
|
93
|
+
sessionPanelRowRecycle: "\u56DE\u6536\u7AD9",
|
|
94
|
+
sessionPanelSelectedCount: "\u5DF2\u9009 {count} \u4E2A",
|
|
91
95
|
sessionPanelSweep: "\u6309\u4FDD\u7559\u7B56\u7565\u6E05\u7406",
|
|
92
96
|
sessionPanelRefresh: "\u5237\u65B0",
|
|
93
97
|
sessionPanelClose: "\u5173\u95ED",
|
|
@@ -100,7 +104,7 @@ var OVER_SSH_DICTS = {
|
|
|
100
104
|
sessionPanelConfirmPurgeBody: "\u5C06\u6C38\u4E45\u6E05\u9664\u56DE\u6536\u7AD9\u6761\u76EE {targets}\uFF0C\u4E0D\u53EF\u6062\u590D\u3002",
|
|
101
105
|
sessionPanelConfirmCancel: "\u53D6\u6D88",
|
|
102
106
|
sessionPanelConfirmAccept: "\u786E\u8BA4\u6C38\u4E45\u5220\u9664",
|
|
103
|
-
sessionPanelDeleteWorkspace: "\u5220\u9664\
|
|
107
|
+
sessionPanelDeleteWorkspace: "\u5220\u9664\u672C\u5DE5\u4F5C\u533A\u2026",
|
|
104
108
|
// 必须是**模板字符串**而不是函数:字典的契约是 Record<OverSshKey, string>,渲染统一走
|
|
105
109
|
// resolveMessage 的 `template.replace(...)`。写成函数会让面板在渲染时抛
|
|
106
110
|
// TypeError: template.replace is not a function,整块白屏。
|
|
@@ -159,6 +163,7 @@ var OVER_SSH_DICTS = {
|
|
|
159
163
|
rootsErrorIdRequired: "\u8BF7\u586B\u5199\u8FDE\u63A5 id\u3002",
|
|
160
164
|
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
165
|
rootsErrorIdDuplicate: "\u8FD9\u4E2A\u8FDE\u63A5 id \u5DF2\u7ECF\u5B58\u5728\u3002",
|
|
166
|
+
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
167
|
rootsErrorHostRequired: "\u8BF7\u586B\u5199\u4E3B\u673A\u540D\u3002",
|
|
163
168
|
rootsErrorUserRequired: "\u8BF7\u586B\u5199\u7528\u6237\u540D\u3002",
|
|
164
169
|
rootsErrorPort: "\u7AEF\u53E3\u5FC5\u987B\u662F 1 \u5230 65535 \u4E4B\u95F4\u7684\u6574\u6570\u3002",
|
|
@@ -245,8 +250,7 @@ var OVER_SSH_DICTS = {
|
|
|
245
250
|
browseChoose: "\u7528\u8FD9\u4E2A\u76EE\u5F55",
|
|
246
251
|
sessionAddOpen: "\u6DFB\u52A0\u76EE\u5F55",
|
|
247
252
|
sessionAddTitle: "\u6DFB\u52A0\u76EE\u5F55\u5230\u5F53\u524D\u5DE5\u4F5C\u533A",
|
|
248
|
-
sessionAddHint: "\
|
|
249
|
-
sessionAddWorkspace: "\u5F53\u524D\u5DE5\u4F5C\u533A\uFF1A{path}",
|
|
253
|
+
sessionAddHint: "\u52A0\u5165\u672C\u4F1A\u8BDD\uFF1A\u6587\u4EF6\u5DE5\u5177\u4E0E @ \u8865\u5168\u7ACB\u5373\u53EF\u7528\uFF0C\u4E0D\u4F1A\u65B0\u5EFA\u5DE5\u4F5C\u533A\u3002",
|
|
250
254
|
sessionAddWorkspaceUnknown: "\u62FF\u4E0D\u5230\u5F53\u524D\u5DE5\u4F5C\u533A\u8DEF\u5F84\uFF0C\u65E0\u6CD5\u628A\u76EE\u5F55\u52A0\u5165\u672C\u4F1A\u8BDD\uFF08\u672C\u7EC4\u5408\u6CA1\u6709\u63D0\u4F9B\u4F1A\u8BDD\u6216\u5DE5\u4F5C\u533A\u670D\u52A1\u65F6\uFF0C\u8FD9\u4E2A\u5165\u53E3\u4E0D\u53EF\u7528\uFF09\u3002",
|
|
251
255
|
sessionAddPick: "\u52A0\u5165\u8FD9\u4E2A\u76EE\u5F55",
|
|
252
256
|
sessionBrowse: "\u6D4F\u89C8\u2026",
|
|
@@ -254,9 +258,9 @@ var OVER_SSH_DICTS = {
|
|
|
254
258
|
sessionRootsTitle: "\u672C\u4F1A\u8BDD\u53EF\u7528\u7684\u76EE\u5F55",
|
|
255
259
|
sessionRootsEmpty: "\u8FD8\u6CA1\u6709\u6DFB\u52A0\u76EE\u5F55\u3002\u70B9\u300C\u6D4F\u89C8\u2026\u300D\u9009\u4E00\u4E2A\u672C\u5730\u6216\u8FDC\u7AEF\u76EE\u5F55\u3002",
|
|
256
260
|
sessionRootsLoading: "\u6B63\u5728\u8BFB\u53D6\u8FD9\u4E2A\u4F1A\u8BDD\u7684\u76EE\u5F55\u2026",
|
|
257
|
-
sessionRootsHint: "\u672C\u5730\
|
|
261
|
+
sessionRootsHint: "\u672C\u5730\u76F4\u63A5\u8BFB\u5199\uFF0C\u8FDC\u7AEF\u7ECF SSH \u4EE3\u7406\u3002\u53EA\u6709\u4F60\u6DFB\u52A0\u7684\u76EE\u5F55\u53EF\u5220\u9664\uFF1B\u63D2\u4EF6\u8BBE\u7F6E\u91CC\u767B\u8BB0\u7684\u6839\u8981\u5148\u5728\u8FD9\u91CC\u6DFB\u52A0\u3002",
|
|
258
262
|
sessionRootsWorkspaceBadge: "\u5DE5\u4F5C\u533A",
|
|
259
|
-
sessionRootsWorkspaceNote: "\
|
|
263
|
+
sessionRootsWorkspaceNote: "\u9ED8\u8BA4\u8FB9\u754C\uFF0C\u4E0D\u53EF\u5220\u9664\uFF08workspace-write \u4E0B\u540C\u5DF2\u6DFB\u52A0\u76EE\u5F55\u4E00\u6837\u53EF\u5199\uFF09",
|
|
260
264
|
sessionRootsFailed: "\u8BFB\u53D6\u76EE\u5F55\u5217\u8868\u5931\u8D25\uFF1A{reason}",
|
|
261
265
|
sessionAddDone: "\u5DF2\u52A0\u5165\uFF1A{path}\uFF08\u672A\u65B0\u5EFA\u5DE5\u4F5C\u533A\uFF09",
|
|
262
266
|
sessionAddFailed: "\u52A0\u5165\u5931\u8D25\uFF1A{reason}",
|
|
@@ -276,6 +280,9 @@ var OVER_SSH_DICTS = {
|
|
|
276
280
|
sessionResultWorkspaceRecycled: "\u5DF2\u628A\u8BE5\u5DE5\u4F5C\u533A {count} \u4E2A\u4F1A\u8BDD\u79FB\u5165\u56DE\u6536\u7AD9\uFF0C{size}\uFF08\u53EF\u6062\u590D\uFF09",
|
|
277
281
|
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
282
|
sessionResultRestored: "\u5DF2\u8FD8\u539F\u8BE5\u4F1A\u8BDD",
|
|
283
|
+
sessionResultRestoredWithSubsessions: "\u5DF2\u8FD8\u539F\u8BE5\u4F1A\u8BDD\uFF0C\u5E76\u8FDE\u5E26\u8FD8\u539F {count} \u4E2A\u5B50\u4F1A\u8BDD",
|
|
284
|
+
sessionResultRestoredPartial: "\u5DF2\u8FD8\u539F\u8BE5\u4F1A\u8BDD\uFF0C\u53E6\u6709 {failed} \u4E2A\u5B50\u4F1A\u8BDD\u4ECD\u7559\u5728\u56DE\u6536\u7AD9",
|
|
285
|
+
sessionResultRestoredParentBinned: "\u5DF2\u8FD8\u539F\u8BE5\u4F1A\u8BDD\uFF08\u5176\u7236\u4F1A\u8BDD\u4ECD\u5728\u56DE\u6536\u7AD9\uFF09",
|
|
279
286
|
sessionResultPurged: "\u5DF2\u6C38\u4E45\u6E05\u9664\u8BE5\u6761\u76EE",
|
|
280
287
|
sessionResultSweepNone: "\u6CA1\u6709\u9700\u8981\u6E05\u7406\u7684\u6761\u76EE",
|
|
281
288
|
sessionResultSweepDone: "\u5DF2\u6E05\u7406 {count} \u4E2A\u6761\u76EE",
|
|
@@ -329,12 +336,16 @@ var OVER_SSH_DICTS = {
|
|
|
329
336
|
sessionPanelSessions: "Sessions",
|
|
330
337
|
sessionPanelBin: "Recycle bin",
|
|
331
338
|
sessionPanelBinEmpty: "The recycle bin is empty.",
|
|
339
|
+
sessionPanelBinUnreadable: "{count} more bin entr(y/ies) cannot be shown (no session id); restart the host or check the bin index.",
|
|
340
|
+
sessionPanelBinRowUnaddressable: "This entry has no id, so it cannot be restored or purged",
|
|
332
341
|
sessionPanelSelectAll: "Select deletable",
|
|
333
342
|
sessionPanelClear: "Clear selection",
|
|
334
343
|
sessionPanelDeleteRecycle: "Delete to bin",
|
|
335
344
|
sessionPanelDeletePermanent: "Delete permanently\u2026",
|
|
336
345
|
sessionPanelRestore: "Restore",
|
|
337
346
|
sessionPanelPurge: "Purge\u2026",
|
|
347
|
+
sessionPanelRowRecycle: "Bin",
|
|
348
|
+
sessionPanelSelectedCount: "{count} selected",
|
|
338
349
|
sessionPanelSweep: "Apply retention policy",
|
|
339
350
|
sessionPanelRefresh: "Refresh",
|
|
340
351
|
sessionPanelClose: "Close",
|
|
@@ -401,6 +412,7 @@ var OVER_SSH_DICTS = {
|
|
|
401
412
|
rootsErrorIdRequired: "Enter a connection id.",
|
|
402
413
|
rootsErrorIdInvalid: "A connection id may contain letters, digits, dots, underscores and dashes, and must start with a letter or a digit.",
|
|
403
414
|
rootsErrorIdDuplicate: "That connection id already exists.",
|
|
415
|
+
rootsErrorConnectionMissing: "That connection has been deleted; close the editor and refresh.",
|
|
404
416
|
rootsErrorHostRequired: "Enter a host name.",
|
|
405
417
|
rootsErrorUserRequired: "Enter a user name.",
|
|
406
418
|
rootsErrorPort: "The port must be an integer between 1 and 65535.",
|
|
@@ -487,8 +499,7 @@ var OVER_SSH_DICTS = {
|
|
|
487
499
|
browseChoose: "Use this directory",
|
|
488
500
|
sessionAddOpen: "Add a directory",
|
|
489
501
|
sessionAddTitle: "Add a directory to the current workspace",
|
|
490
|
-
sessionAddHint: "
|
|
491
|
-
sessionAddWorkspace: "Current workspace: {path}",
|
|
502
|
+
sessionAddHint: "Added to this session: its file tools and @-completion work immediately; no new workspace is created.",
|
|
492
503
|
sessionAddWorkspaceUnknown: "The current workspace path is unavailable, so the directory cannot be added to this session (when the composition provides neither the session nor the workspace service, this entry cannot work at all).",
|
|
493
504
|
sessionAddPick: "Add this directory",
|
|
494
505
|
sessionBrowse: "Browse\u2026",
|
|
@@ -518,6 +529,9 @@ var OVER_SSH_DICTS = {
|
|
|
518
529
|
sessionResultWorkspaceRecycled: "Moved {count} sessions of that workspace to the recycle bin, {size} (recoverable)",
|
|
519
530
|
sessionResultWorkspaceRecycledRefused: "Moved {count} sessions of that workspace to the recycle bin, {size} (recoverable; {refused} refused)",
|
|
520
531
|
sessionResultRestored: "Restored that session",
|
|
532
|
+
sessionResultRestoredWithSubsessions: "Restored that session and {count} sub-session(s)",
|
|
533
|
+
sessionResultRestoredPartial: "Restored that session; {failed} sub-session(s) stayed in the bin",
|
|
534
|
+
sessionResultRestoredParentBinned: "Restored that session (its parent is still in the bin)",
|
|
521
535
|
sessionResultPurged: "Permanently removed that entry",
|
|
522
536
|
sessionResultSweepNone: "Nothing to sweep",
|
|
523
537
|
sessionResultSweepDone: "Swept {count} entries",
|
|
@@ -688,6 +702,8 @@ var CLASS = {
|
|
|
688
702
|
group: "dsh-rw-group",
|
|
689
703
|
/** A workspace section's heading line (label + count). */
|
|
690
704
|
groupHead: "dsh-rw-group-head",
|
|
705
|
+
/** The workspace directory, inline in the group head: one line, ellipsized, full path in `title`. */
|
|
706
|
+
groupPath: "dsh-rw-group-path",
|
|
691
707
|
/**
|
|
692
708
|
* The inventory's action bar (select all / clear / delete). Sticky: the list is long, and a bar that
|
|
693
709
|
* scrolls away forces the user to scroll back up for every action.
|
|
@@ -939,6 +955,7 @@ function controlStylesheet() {
|
|
|
939
955
|
`.${CLASS.group} { display: flex; flex-direction: column; gap: ${SPACE.xs}; box-sizing: border-box; min-width: 0; max-width: 100%; padding: ${SPACE.sm}; background: ${TOKENS.bgLayer2}; border: 1px solid ${TOKENS.border}; border-radius: ${TOKENS.radiusMd}; }`,
|
|
940
956
|
/* The heading is separated from the rows it labels, so a long list has a visible rhythm. */
|
|
941
957
|
`.${CLASS.groupHead} { display: flex; flex-wrap: wrap; align-items: baseline; gap: ${SPACE.xs}; min-width: 0; padding-bottom: ${SPACE.hair}; border-bottom: 1px solid ${TOKENS.border}; }`,
|
|
958
|
+
`.${CLASS.groupPath} { flex: 1 1 12rem; min-width: 0; font-size: 0.75rem; line-height: 1.5; color: ${TOKENS.textTertiary}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }`,
|
|
942
959
|
/**
|
|
943
960
|
* The session inventory's action bar, sticky against **whatever scrolls it**.
|
|
944
961
|
*
|
|
@@ -1438,15 +1455,29 @@ function createCardPortal(options) {
|
|
|
1438
1455
|
};
|
|
1439
1456
|
}
|
|
1440
1457
|
function focusFirstControl(container) {
|
|
1458
|
+
const all = container?.querySelectorAll?.(CARD_FOCUS_SELECTOR);
|
|
1459
|
+
if (all !== void 0 && all !== null) {
|
|
1460
|
+
for (const candidate of Array.from(all)) {
|
|
1461
|
+
if (!isVisibleControl(candidate)) continue;
|
|
1462
|
+
candidate.focus?.();
|
|
1463
|
+
return true;
|
|
1464
|
+
}
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1441
1467
|
const target = container?.querySelector?.(CARD_FOCUS_SELECTOR) ?? void 0;
|
|
1442
1468
|
if (target === void 0 || typeof target.focus !== "function") return false;
|
|
1443
1469
|
target.focus();
|
|
1444
1470
|
return true;
|
|
1445
1471
|
}
|
|
1472
|
+
function isVisibleControl(element) {
|
|
1473
|
+
const measurable = element.getClientRects;
|
|
1474
|
+
if (typeof measurable === "function") return measurable.call(element).length > 0;
|
|
1475
|
+
return element.hidden !== true;
|
|
1476
|
+
}
|
|
1446
1477
|
function focusableWithin(container) {
|
|
1447
1478
|
const found = container?.querySelectorAll?.(CARD_FOCUS_SELECTOR);
|
|
1448
1479
|
if (found === null || found === void 0) return [];
|
|
1449
|
-
return Array.from(found);
|
|
1480
|
+
return Array.from(found).filter(isVisibleControl);
|
|
1450
1481
|
}
|
|
1451
1482
|
function trapTab(event, container, active) {
|
|
1452
1483
|
const focusables = focusableWithin(container);
|
|
@@ -2726,6 +2757,11 @@ function createRootsPanelController(options) {
|
|
|
2726
2757
|
const editingId = state.editingId;
|
|
2727
2758
|
if (editingId === void 0) return;
|
|
2728
2759
|
if (!canEdit(state)) return;
|
|
2760
|
+
if (!(state.snapshot?.connections ?? []).some((row) => row.id === editingId)) {
|
|
2761
|
+
dispatch({ type: "edit-ended" });
|
|
2762
|
+
dispatch({ type: "refused", scope: "edit", message: { key: "rootsErrorConnectionMissing" } });
|
|
2763
|
+
return;
|
|
2764
|
+
}
|
|
2729
2765
|
const editDrafts = state.editDrafts;
|
|
2730
2766
|
const portText = editDrafts.port.trim();
|
|
2731
2767
|
const port = portText === "" ? void 0 : Number(portText);
|
|
@@ -3869,10 +3905,10 @@ function toSessionRowView(raw) {
|
|
|
3869
3905
|
};
|
|
3870
3906
|
}
|
|
3871
3907
|
function toBinRowView(raw) {
|
|
3872
|
-
const
|
|
3908
|
+
const rawEntryId = raw["entryId"];
|
|
3873
3909
|
const sessionId = raw["sessionId"];
|
|
3874
|
-
if (typeof entryId !== "string" || entryId === "") return void 0;
|
|
3875
3910
|
if (typeof sessionId !== "string" || sessionId === "") return void 0;
|
|
3911
|
+
const entryId = typeof rawEntryId === "string" ? rawEntryId : "";
|
|
3876
3912
|
return {
|
|
3877
3913
|
entryId,
|
|
3878
3914
|
sessionId,
|
|
@@ -3887,6 +3923,8 @@ function emptySnapshot() {
|
|
|
3887
3923
|
available: false,
|
|
3888
3924
|
sessions: [],
|
|
3889
3925
|
bin: [],
|
|
3926
|
+
binUnaddressable: 0,
|
|
3927
|
+
binUnreadable: 0,
|
|
3890
3928
|
policy: { maxEntries: 0, maxBytes: 0, maxAgeMs: 0 },
|
|
3891
3929
|
totals: { sessions: 0, bytes: 0, subsessions: 0, live: 0, running: 0 }
|
|
3892
3930
|
};
|
|
@@ -3949,7 +3987,8 @@ function formatStamp(ms, now = Date.now()) {
|
|
|
3949
3987
|
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
3950
3988
|
}
|
|
3951
3989
|
function shortId(id) {
|
|
3952
|
-
|
|
3990
|
+
const body = id.startsWith("session-") ? id.slice("session-".length) : id;
|
|
3991
|
+
return body.length <= 8 ? body : body.slice(0, 8);
|
|
3953
3992
|
}
|
|
3954
3993
|
function parseSessionSnapshot(value) {
|
|
3955
3994
|
if (typeof value !== "object" || value === null) return emptySnapshot();
|
|
@@ -3958,13 +3997,28 @@ function parseSessionSnapshot(value) {
|
|
|
3958
3997
|
const available = body["available"] === true;
|
|
3959
3998
|
const sessions = Array.isArray(body["sessions"]) ? body["sessions"].flatMap((raw) => typeof raw === "object" && raw !== null ? [toSessionRowView(raw)].filter((row) => row !== void 0) : []) : [];
|
|
3960
3999
|
const unblocked = unblockOrphanSubsessions(sessions);
|
|
3961
|
-
|
|
4000
|
+
let binUnreadable = 0;
|
|
4001
|
+
const bin = Array.isArray(body["bin"]) ? body["bin"].flatMap((raw) => {
|
|
4002
|
+
if (typeof raw !== "object" || raw === null) {
|
|
4003
|
+
binUnreadable += 1;
|
|
4004
|
+
return [];
|
|
4005
|
+
}
|
|
4006
|
+
const row = toBinRowView(raw);
|
|
4007
|
+
if (row === void 0) {
|
|
4008
|
+
binUnreadable += 1;
|
|
4009
|
+
return [];
|
|
4010
|
+
}
|
|
4011
|
+
return [row];
|
|
4012
|
+
}) : [];
|
|
4013
|
+
const binUnaddressable = bin.filter((row) => row.entryId === "").length;
|
|
3962
4014
|
const rawPolicy = typeof body["policy"] === "object" && body["policy"] !== null ? body["policy"] : {};
|
|
3963
4015
|
const numberOr = (key) => typeof rawPolicy[key] === "number" && Number.isFinite(rawPolicy[key]) ? rawPolicy[key] : 0;
|
|
3964
4016
|
return {
|
|
3965
4017
|
available,
|
|
3966
4018
|
sessions: unblocked,
|
|
3967
4019
|
bin,
|
|
4020
|
+
binUnaddressable,
|
|
4021
|
+
binUnreadable,
|
|
3968
4022
|
policy: { maxEntries: numberOr("maxEntries"), maxBytes: numberOr("maxBytes"), maxAgeMs: numberOr("maxAgeMs") },
|
|
3969
4023
|
totals: {
|
|
3970
4024
|
sessions: unblocked.length,
|
|
@@ -4049,7 +4103,14 @@ async function deleteWorkspaceSessions(io, cwd, options = {}) {
|
|
|
4049
4103
|
}
|
|
4050
4104
|
async function restoreSession(io, entryId) {
|
|
4051
4105
|
if (entryId === "") return { kind: "error", message: { key: "sessionResultMissingEntryId" } };
|
|
4052
|
-
return postAction(io, { action: "restore", entryId }, () =>
|
|
4106
|
+
return postAction(io, { action: "restore", entryId }, (value) => {
|
|
4107
|
+
const failed = typeof value["failedSubsessions"] === "number" ? value["failedSubsessions"] : 0;
|
|
4108
|
+
const restored = typeof value["restoredSubsessions"] === "number" ? value["restoredSubsessions"] : 0;
|
|
4109
|
+
if (failed > 0) return { key: "sessionResultRestoredPartial", params: { failed } };
|
|
4110
|
+
if (restored > 0) return { key: "sessionResultRestoredWithSubsessions", params: { count: restored } };
|
|
4111
|
+
if (value["parentStillBinned"] === true) return { key: "sessionResultRestoredParentBinned" };
|
|
4112
|
+
return { key: "sessionResultRestored" };
|
|
4113
|
+
});
|
|
4053
4114
|
}
|
|
4054
4115
|
async function purgeSession(io, entryId) {
|
|
4055
4116
|
if (entryId === "") return { kind: "error", message: { key: "sessionResultMissingEntryId" } };
|
|
@@ -4108,7 +4169,7 @@ function badgeFor(row, copy) {
|
|
|
4108
4169
|
return void 0;
|
|
4109
4170
|
}
|
|
4110
4171
|
function sessionRowTemplate(props) {
|
|
4111
|
-
const { row, copy, checked, onToggle, pickIdPrefix } = props;
|
|
4172
|
+
const { row, copy, checked, onToggle, pickIdPrefix, onRecycle } = props;
|
|
4112
4173
|
const badge = badgeFor(row, copy);
|
|
4113
4174
|
const stamp = formatStamp(row.updatedAtMs);
|
|
4114
4175
|
return (0, import_react7.createElement)(
|
|
@@ -4150,7 +4211,15 @@ function sessionRowTemplate(props) {
|
|
|
4150
4211
|
(0, import_react7.createElement)("span", void 0, formatBytes(row.bytes))
|
|
4151
4212
|
)
|
|
4152
4213
|
)
|
|
4153
|
-
)
|
|
4214
|
+
),
|
|
4215
|
+
// 便捷手段:勾选 + 顶部批量按钮对"只想删一条"的场景太重。回收站是**可恢复**的,所以这里不弹确认,
|
|
4216
|
+
// 直接移入回收站(永久删除仍然只在批量流程里,且带确认)。
|
|
4217
|
+
row.deletable && onRecycle !== void 0 ? Button({
|
|
4218
|
+
variant: "ghost",
|
|
4219
|
+
size: "compact",
|
|
4220
|
+
onClick: () => onRecycle(row.id),
|
|
4221
|
+
children: copy.rowRecycle
|
|
4222
|
+
}) : null
|
|
4154
4223
|
);
|
|
4155
4224
|
}
|
|
4156
4225
|
var SessionRow = (0, import_react7.memo)(function SessionRow2(props) {
|
|
@@ -4158,9 +4227,10 @@ var SessionRow = (0, import_react7.memo)(function SessionRow2(props) {
|
|
|
4158
4227
|
});
|
|
4159
4228
|
function renderBinRow(props) {
|
|
4160
4229
|
const { row, copy, busy } = props;
|
|
4230
|
+
const unaddressable = row.entryId === "";
|
|
4161
4231
|
return (0, import_react7.createElement)(
|
|
4162
4232
|
"li",
|
|
4163
|
-
{ key: row.entryId },
|
|
4233
|
+
{ key: unaddressable ? `unaddressable:${row.sessionId}` : row.entryId },
|
|
4164
4234
|
(0, import_react7.createElement)(
|
|
4165
4235
|
"div",
|
|
4166
4236
|
{ className: CLASS.row },
|
|
@@ -4172,20 +4242,22 @@ function renderBinRow(props) {
|
|
|
4172
4242
|
(0, import_react7.createElement)(
|
|
4173
4243
|
"span",
|
|
4174
4244
|
{ className: CLASS.rowActions },
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4245
|
+
...unaddressable ? [(0, import_react7.createElement)("span", { className: CLASS.rowMeta, title: copy.binRowUnaddressable }, copy.binRowUnaddressable)] : [
|
|
4246
|
+
Button({
|
|
4247
|
+
variant: "secondary",
|
|
4248
|
+
size: "compact",
|
|
4249
|
+
disabled: busy,
|
|
4250
|
+
onClick: () => props.onRestore(row.entryId),
|
|
4251
|
+
children: copy.restore
|
|
4252
|
+
}),
|
|
4253
|
+
Button({
|
|
4254
|
+
variant: "ghost",
|
|
4255
|
+
size: "compact",
|
|
4256
|
+
disabled: busy,
|
|
4257
|
+
onClick: () => props.onPurge(row.entryId),
|
|
4258
|
+
children: copy.purge
|
|
4259
|
+
})
|
|
4260
|
+
]
|
|
4189
4261
|
)
|
|
4190
4262
|
)
|
|
4191
4263
|
);
|
|
@@ -4202,6 +4274,8 @@ function renderWorkspaceGroup(props) {
|
|
|
4202
4274
|
{ className: CLASS.groupHead },
|
|
4203
4275
|
(0, import_react7.createElement)("span", { className: CLASS.rowTitle }, group.label === "" ? copy.workspaceUnknown : group.label),
|
|
4204
4276
|
(0, import_react7.createElement)("span", { className: CLASS.badge }, String(group.sessions.length)),
|
|
4277
|
+
// 目录内联在组头里(可省略号截断,完整路径留在 title),不再让组头下面多出整整一行只放路径。
|
|
4278
|
+
group.cwd === void 0 ? null : (0, import_react7.createElement)("span", { className: CLASS.groupPath, title: group.cwd }, group.cwd),
|
|
4205
4279
|
// A workspace with no directory (the platform's no-cwd bucket) has nothing to scope a delete to, so it
|
|
4206
4280
|
// gets no button rather than a button that guesses.
|
|
4207
4281
|
remove === void 0 ? null : Button({
|
|
@@ -4212,7 +4286,6 @@ function renderWorkspaceGroup(props) {
|
|
|
4212
4286
|
children: copy.deleteWorkspace
|
|
4213
4287
|
})
|
|
4214
4288
|
),
|
|
4215
|
-
group.cwd === void 0 ? null : (0, import_react7.createElement)("p", { className: CLASS.hint, title: group.cwd }, group.cwd),
|
|
4216
4289
|
(0, import_react7.createElement)(
|
|
4217
4290
|
"ul",
|
|
4218
4291
|
{ className: CLASS.list },
|
|
@@ -4225,6 +4298,7 @@ function renderWorkspaceGroup(props) {
|
|
|
4225
4298
|
copy,
|
|
4226
4299
|
checked: selectedSet.has(row.id),
|
|
4227
4300
|
onToggle: props.onToggle,
|
|
4301
|
+
...props.onRecycle === void 0 ? {} : { onRecycle: props.onRecycle },
|
|
4228
4302
|
pickIdPrefix: props.pickIdPrefix
|
|
4229
4303
|
})
|
|
4230
4304
|
)
|
|
@@ -4428,6 +4502,7 @@ function SessionPanel({ copy, io, close }) {
|
|
|
4428
4502
|
children: copy.selectAll
|
|
4429
4503
|
}),
|
|
4430
4504
|
Button({ variant: "ghost", size: "compact", onClick: () => setSelected([]), children: copy.clearSelection }),
|
|
4505
|
+
deletable.length === 0 ? null : (0, import_react7.createElement)("span", { className: CLASS.rowMeta }, copy.selectedCount(deletable.length)),
|
|
4431
4506
|
Button({
|
|
4432
4507
|
variant: "primary",
|
|
4433
4508
|
size: "compact",
|
|
@@ -4453,6 +4528,7 @@ function SessionPanel({ copy, io, close }) {
|
|
|
4453
4528
|
selected,
|
|
4454
4529
|
busy,
|
|
4455
4530
|
onToggle: toggle,
|
|
4531
|
+
onRecycle: (id) => void run(() => deleteSessions(io, [id])),
|
|
4456
4532
|
onRemove: setPending,
|
|
4457
4533
|
pickIdPrefix
|
|
4458
4534
|
})
|
|
@@ -4463,10 +4539,10 @@ function SessionPanel({ copy, io, close }) {
|
|
|
4463
4539
|
(0, import_react7.createElement)("h3", { className: CLASS.blockTitle }, copy.binHeading),
|
|
4464
4540
|
// Same rule as the inventory: 「回收站为空」 is a claim about an **answer**. While nothing has been read (or the
|
|
4465
4541
|
// read failed) the status line above says what is happening (audit 2026-11-01b ui P1-3).
|
|
4466
|
-
phase.kind !== "ready" ? null : snapshot.bin.length === 0 ? (0, import_react7.createElement)("p", { className: CLASS.empty }, copy.binEmpty) : (0, import_react7.createElement)(
|
|
4542
|
+
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)(
|
|
4467
4543
|
"div",
|
|
4468
4544
|
{ className: CLASS.block },
|
|
4469
|
-
(0, import_react7.createElement)(
|
|
4545
|
+
snapshot.bin.length === 0 ? null : (0, import_react7.createElement)(
|
|
4470
4546
|
"ul",
|
|
4471
4547
|
{ className: CLASS.list },
|
|
4472
4548
|
...snapshot.bin.map(
|
|
@@ -4479,6 +4555,7 @@ function SessionPanel({ copy, io, close }) {
|
|
|
4479
4555
|
})
|
|
4480
4556
|
)
|
|
4481
4557
|
),
|
|
4558
|
+
snapshot.binUnreadable > 0 ? (0, import_react7.createElement)("p", { className: CLASS.empty }, copy.binUnreadable(snapshot.binUnreadable)) : null,
|
|
4482
4559
|
Button({
|
|
4483
4560
|
variant: "secondary",
|
|
4484
4561
|
size: "compact",
|
|
@@ -5558,7 +5635,15 @@ function createSessionDirectoryIo(options) {
|
|
|
5558
5635
|
}
|
|
5559
5636
|
const all = result.value.roots;
|
|
5560
5637
|
if (sessionId !== void 0 && sessionId !== "" && result.value.scopedForSession === sessionId) {
|
|
5561
|
-
return { ok: true, value: all };
|
|
5638
|
+
if (workspacePath === void 0 || workspacePath === "") return { ok: true, value: all };
|
|
5639
|
+
return {
|
|
5640
|
+
ok: true,
|
|
5641
|
+
value: all.filter((row) => {
|
|
5642
|
+
const sessions = row.sessions ?? [];
|
|
5643
|
+
if (sessions.length > 0) return true;
|
|
5644
|
+
return (row.workspaces ?? []).some((candidate) => sameWorkspaceSpelling(candidate, workspacePath));
|
|
5645
|
+
})
|
|
5646
|
+
};
|
|
5562
5647
|
}
|
|
5563
5648
|
if (workspacePath === void 0 || workspacePath === "") return { ok: true, value: [] };
|
|
5564
5649
|
return {
|
|
@@ -5566,7 +5651,7 @@ function createSessionDirectoryIo(options) {
|
|
|
5566
5651
|
value: all.filter((row) => {
|
|
5567
5652
|
const sessions = row.sessions ?? [];
|
|
5568
5653
|
if (sessions.length > 0 && sessionId !== void 0 && sessionId !== "") return sessions.includes(sessionId);
|
|
5569
|
-
return row.workspaces
|
|
5654
|
+
return (row.workspaces ?? []).some((candidate) => sameWorkspaceSpelling(candidate, workspacePath));
|
|
5570
5655
|
})
|
|
5571
5656
|
};
|
|
5572
5657
|
};
|
|
@@ -5623,16 +5708,26 @@ function sessionDirectoryReducer(state, action) {
|
|
|
5623
5708
|
const next = action.path === void 0 || action.path === "" ? void 0 : action.path;
|
|
5624
5709
|
if (state.workspacePath === next) return state;
|
|
5625
5710
|
const base = withoutOptional2(state);
|
|
5711
|
+
const switched = state.workspacePath !== void 0 && next !== void 0 && state.workspacePath !== next;
|
|
5712
|
+
const cleared = {
|
|
5713
|
+
...base,
|
|
5714
|
+
roots: [],
|
|
5715
|
+
// The card/browser state is also entry-shared. A move between two real workspaces must not carry
|
|
5716
|
+
// the previous card's open/browse state into the new one; a late initial seat (undefined -> value)
|
|
5717
|
+
// must NOT close a card the user already opened.
|
|
5718
|
+
...switched ? { open: false, browsing: false } : {}
|
|
5719
|
+
};
|
|
5626
5720
|
if (next === void 0) {
|
|
5627
|
-
const { workspacePath: _cleared, ...rest } =
|
|
5721
|
+
const { workspacePath: _cleared, ...rest } = cleared;
|
|
5628
5722
|
return rest;
|
|
5629
5723
|
}
|
|
5630
|
-
return { ...
|
|
5724
|
+
return { ...cleared, workspacePath: next };
|
|
5631
5725
|
}
|
|
5632
5726
|
case "session-known": {
|
|
5633
5727
|
const next = action.sessionId === void 0 || action.sessionId === "" ? void 0 : action.sessionId;
|
|
5634
5728
|
if (state.sessionId === next) return state;
|
|
5635
|
-
const
|
|
5729
|
+
const switched = state.sessionId !== void 0 && next !== void 0 && state.sessionId !== next;
|
|
5730
|
+
const base = { ...withoutOptional2(state), roots: [], ...switched ? { open: false, browsing: false } : {} };
|
|
5636
5731
|
if (next === void 0) {
|
|
5637
5732
|
const { sessionId: _cleared, ...rest } = base;
|
|
5638
5733
|
return rest;
|
|
@@ -5677,15 +5772,23 @@ function sessionDirectoryReducer(state, action) {
|
|
|
5677
5772
|
};
|
|
5678
5773
|
}
|
|
5679
5774
|
}
|
|
5775
|
+
function sameWorkspaceSpelling(left, right) {
|
|
5776
|
+
const fold = (value) => value.replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\/$/, "");
|
|
5777
|
+
const a = fold(left);
|
|
5778
|
+
const b = fold(right);
|
|
5779
|
+
if (a === b) return true;
|
|
5780
|
+
return /^[A-Za-z]:\//.test(a) && /^[A-Za-z]:\//.test(b) && a.toLowerCase() === b.toLowerCase();
|
|
5781
|
+
}
|
|
5680
5782
|
function selectSessionDirectoryView(state, copy) {
|
|
5681
5783
|
const view = {
|
|
5682
5784
|
visible: state.open,
|
|
5683
5785
|
pending: state.pending,
|
|
5684
5786
|
notice: resolveMessage2(copy, state.notice),
|
|
5685
|
-
workspaceLine: state.workspacePath === void 0 ? copy["sessionAddWorkspaceUnknown"] : resolveMessage2(copy, { key: "sessionAddWorkspace", params: { path: state.workspacePath } }),
|
|
5686
5787
|
...state.workspacePath === void 0 ? {} : { workspacePath: state.workspacePath },
|
|
5687
5788
|
...state.added === void 0 ? {} : { added: state.added },
|
|
5688
5789
|
// 工作区永远排在第一行:它是默认边界,`workspace-write` 下与已添加目录一样可写(ADR-0016)。
|
|
5790
|
+
// Filter again at render time: the controller is entry-scoped and shared by every session card, so a
|
|
5791
|
+
// stale `roots` snapshot from the previous session must not paint a row scoped to another workspace.
|
|
5689
5792
|
roots: [
|
|
5690
5793
|
...state.workspacePath === void 0 ? [] : [{
|
|
5691
5794
|
key: `workspace:${state.workspacePath}`,
|
|
@@ -5695,7 +5798,9 @@ function selectSessionDirectoryView(state, copy) {
|
|
|
5695
5798
|
removable: false,
|
|
5696
5799
|
workspace: true
|
|
5697
5800
|
}],
|
|
5698
|
-
...state.roots.
|
|
5801
|
+
...state.roots.filter(
|
|
5802
|
+
(row) => row.workspaces === void 0 || row.workspaces.length === 0 || state.workspacePath === void 0 || state.workspacePath === "" || row.workspaces.some((candidate) => sameWorkspaceSpelling(candidate, state.workspacePath))
|
|
5803
|
+
).map((row) => ({
|
|
5699
5804
|
key: row.key,
|
|
5700
5805
|
// A label is what the user called it; without one the path is the only honest title. A remote path alone
|
|
5701
5806
|
// would hide which host it is on, so the connection name is part of the title there.
|
|
@@ -5857,22 +5962,13 @@ function SessionDirectorySection({
|
|
|
5857
5962
|
},
|
|
5858
5963
|
(0, import_react11.createElement)("h3", { id: titleId, className: CLASS.sectionTitle }, copy.sessionAddTitle),
|
|
5859
5964
|
(0, import_react11.createElement)("p", { className: CLASS.hint }, copy.sessionAddHint),
|
|
5860
|
-
//
|
|
5861
|
-
//
|
|
5862
|
-
(0, import_react11.createElement)("p", { className: CLASS.
|
|
5965
|
+
// 工作区路径只出现一次:列表第一行就是它(带「工作区」徽标)。此前这里还有一行“当前工作区:…”,
|
|
5966
|
+
// 与第一行完全重复;拿不到路径时仍然要明说,否则用户不知道为什么会话加不了目录。
|
|
5967
|
+
view.workspacePath === void 0 ? (0, import_react11.createElement)("p", { className: CLASS.empty }, copy["sessionAddWorkspaceUnknown"]) : null,
|
|
5863
5968
|
// The card no longer *is* a browser: it lists what this session has and offers one way in. The picker is a
|
|
5864
5969
|
// step behind 「浏览…」, which also means the card no longer opens with a full directory listing (that was
|
|
5865
5970
|
// the complaint: the panel showed the current directory's contents before the user asked for anything).
|
|
5866
|
-
view.browsing ? (0, import_react11.createElement)(
|
|
5867
|
-
"div",
|
|
5868
|
-
{ className: CLASS.block },
|
|
5869
|
-
browser,
|
|
5870
|
-
(0, import_react11.createElement)(
|
|
5871
|
-
"div",
|
|
5872
|
-
{ className: CLASS.actions },
|
|
5873
|
-
Button({ variant: "ghost", size: "compact", onClick: actions.onBrowse, children: copy.sessionBrowseBack })
|
|
5874
|
-
)
|
|
5875
|
-
) : (0, import_react11.createElement)(
|
|
5971
|
+
view.browsing ? (0, import_react11.createElement)("div", { className: CLASS.block }, browser) : (0, import_react11.createElement)(
|
|
5876
5972
|
"div",
|
|
5877
5973
|
{ className: CLASS.block },
|
|
5878
5974
|
(0, import_react11.createElement)("h4", { className: CLASS.blockTitle }, copy.sessionRootsTitle),
|
|
@@ -5911,17 +6007,18 @@ function SessionDirectorySection({
|
|
|
5911
6007
|
)
|
|
5912
6008
|
),
|
|
5913
6009
|
// 列表里只剩工作区行时,"还没有添加目录"仍然是有用的下一步提示;但它不再替换列表。
|
|
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
|
|
5915
|
-
(0, import_react11.createElement)(
|
|
5916
|
-
"div",
|
|
5917
|
-
{ className: CLASS.actions },
|
|
5918
|
-
Button({ variant: "secondary", size: "compact", onClick: actions.onBrowse, children: copy.sessionBrowse })
|
|
5919
|
-
)
|
|
6010
|
+
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
|
|
5920
6011
|
),
|
|
5921
6012
|
view.added === void 0 ? null : Notice({ role: "status", tone: "info", children: resolveMessage2(copy, { key: "sessionAddDone", params: { path: view.added } }) }),
|
|
5922
6013
|
view.pending ? Notice({ role: "status", tone: "info", children: copy.flowPending }) : null,
|
|
5923
6014
|
view.notice === "" ? null : Notice({ role: "alert", tone: "error", children: view.notice }),
|
|
5924
|
-
|
|
6015
|
+
// 主操作与关闭收在底部同一行:此前「浏览…」长在列表块里、关闭单独一行,两个按钮上下分离。
|
|
6016
|
+
(0, import_react11.createElement)(
|
|
6017
|
+
"div",
|
|
6018
|
+
{ className: CLASS.actions },
|
|
6019
|
+
view.browsing ? Button({ variant: "secondary", size: "compact", onClick: actions.onBrowse, children: copy.sessionBrowseBack }) : Button({ variant: "secondary", size: "compact", onClick: actions.onBrowse, children: copy.sessionBrowse }),
|
|
6020
|
+
Button({ variant: "secondary", size: "compact", onClick: actions.onClose, children: copy.close })
|
|
6021
|
+
)
|
|
5925
6022
|
);
|
|
5926
6023
|
return (0, import_react11.createElement)(
|
|
5927
6024
|
"div",
|
|
@@ -6634,12 +6731,16 @@ function apply(ctx) {
|
|
|
6634
6731
|
sessionsHeading: dict.sessionPanelSessions,
|
|
6635
6732
|
binHeading: dict.sessionPanelBin,
|
|
6636
6733
|
binEmpty: dict.sessionPanelBinEmpty,
|
|
6734
|
+
binUnreadable: (count) => resolveMessage2(dict, { key: "sessionPanelBinUnreadable", params: { count } }),
|
|
6735
|
+
binRowUnaddressable: dict.sessionPanelBinRowUnaddressable,
|
|
6637
6736
|
selectAll: dict.sessionPanelSelectAll,
|
|
6638
6737
|
clearSelection: dict.sessionPanelClear,
|
|
6639
6738
|
deleteRecycle: dict.sessionPanelDeleteRecycle,
|
|
6640
6739
|
deletePermanent: dict.sessionPanelDeletePermanent,
|
|
6641
6740
|
restore: dict.sessionPanelRestore,
|
|
6642
6741
|
purge: dict.sessionPanelPurge,
|
|
6742
|
+
rowRecycle: dict.sessionPanelRowRecycle,
|
|
6743
|
+
selectedCount: (count) => resolveMessage2(dict, { key: "sessionPanelSelectedCount", params: { count } }),
|
|
6643
6744
|
sweep: dict.sessionPanelSweep,
|
|
6644
6745
|
refresh: dict.sessionPanelRefresh,
|
|
6645
6746
|
close: dict.sessionPanelClose,
|