dsh-archived 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,931 @@
1
+ // dsh-archived browser half.
2
+ //
3
+ // Built in the DSH lazy-CJS bundle format: executing this file only registers a
4
+ // factory; the module body (styles, locale, slot registration) runs when the
5
+ // browser materializes the module.
6
+ //
7
+ // It contributes the Settings → Archived page. On DSH 0.2.0-rc.2 the shipped
8
+ // archived-sessions settings page no longer exists, so this is an ordinary
9
+ // list-slot contribution with no shadowing to do; the id and `priority: -1`
10
+ // are kept so that on an older host which still ships the page, the lowest
11
+ // priority still wins the cell.
12
+ //
13
+ // Rows come from the host route, not from the browser's session store: the
14
+ // archive set can name an id whose summary this client has never loaded (an
15
+ // "index-only" leftover), and a page that silently drops those rows is exactly
16
+ // the page that made them impossible to get rid of.
17
+
18
+ window.__ModuleLoader__.load({
19
+ id: "dsh-archived",
20
+ factory: (require) => {
21
+ var module = { exports: {} };
22
+ var exports = module.exports;
23
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
24
+
25
+ var React = require("react");
26
+ var h = React.createElement;
27
+ var useState = React.useState;
28
+ var useEffect = React.useEffect;
29
+ var useMemo = React.useMemo;
30
+
31
+ var BASE = "/api/dsh-archived";
32
+ var STATE_PATH = BASE + "/state";
33
+ var DELETE_PATH = BASE + "/delete";
34
+ var DELETE_ALL_PATH = BASE + "/delete-all";
35
+ var RESTORE_PATH = BASE + "/restore";
36
+ var EMPTY_PATH = BASE + "/empty-quarantine";
37
+ var REVEAL_PATH = BASE + "/reveal";
38
+ var HEADER = "x-dsh-archived";
39
+ var API_VERSION = 2;
40
+ var STYLE_ID = "dsh-archived-style";
41
+
42
+ var CSS = [
43
+ ".dasm_section{width:100%;max-width:760px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}",
44
+ ".dasm_title{font-size:15px;line-height:22px;font-weight:600;margin:0}",
45
+ ".dasm_head{align-items:center;gap:8px;display:flex}",
46
+ ".dasm_search{flex:1;color:var(--dsw-alias-label-tertiary);align-items:center;display:flex;position:relative}",
47
+ ".dasm_search>svg{pointer-events:none;position:absolute;left:10px}",
48
+ ".dasm_search input{border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);width:100%;height:32px;color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;padding:0 12px 0 34px}",
49
+ ".dasm_tools{align-items:center;gap:6px;flex-wrap:wrap;display:flex}",
50
+ ".dasm_status{color:var(--dsw-alias-label-tertiary);margin:0;font-size:13px;line-height:20px}",
51
+ ".dasm_notice{color:var(--dsw-alias-state-success-primary,#36d67a);margin:0;font-size:12px;line-height:18px;word-break:break-word}",
52
+ ".dasm_noticeError{color:var(--dsw-alias-state-error-primary,#ff6464);margin:0;font-size:12px;line-height:18px;word-break:break-word}",
53
+ ".dasm_group{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:6px 0 0}",
54
+ ".dasm_list{flex-direction:column;gap:2px;margin:0;padding:0;list-style:none;display:flex}",
55
+ ".dasm_row{border-radius:8px;align-items:flex-start;gap:10px;padding:8px 10px;display:flex}",
56
+ ".dasm_row:hover{background:var(--dsw-alias-bg-layer-1)}",
57
+ ".dasm_check{margin-top:3px;flex:0 0 auto}",
58
+ ".dasm_identity{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}",
59
+ ".dasm_title{text-overflow:ellipsis;white-space:nowrap;font-size:13px;line-height:20px;overflow:hidden}",
60
+ ".dasm_meta{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
61
+ ".dasm_detail{color:var(--dsw-alias-label-secondary,#b8bbc2);font-size:12px;line-height:18px;white-space:pre-wrap;word-break:break-word}",
62
+ ".dasm_actions{align-items:center;gap:6px;flex:0 0 auto;display:flex;flex-wrap:wrap;justify-content:flex-end}",
63
+ ".dasm_hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
64
+ ".dasm_btn{display:inline-flex;align-items:center;justify-content:center;min-height:28px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2,#4b4d52);border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary,#b8bbc2);font:inherit;font-size:13px;line-height:18px;white-space:nowrap;cursor:pointer}",
65
+ ".dasm_btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#3a3b3f);color:var(--dsw-alias-label-primary,#fff)}",
66
+ ".dasm_btn:disabled{cursor:not-allowed;opacity:.55}",
67
+ ".dasm_btnDanger{border-color:var(--dsw-alias-state-error-primary,#ff6464);color:var(--dsw-alias-state-error-primary,#ff6464)}",
68
+ ".dasm_btnDanger:hover:not(:disabled){background:var(--dsw-alias-state-error-primary,#ff6464);color:#fff}",
69
+ ".dasm_btnSolidDanger{border-color:var(--dsw-alias-state-error-primary,#ff6464);background:var(--dsw-alias-state-error-primary,#ff6464);color:#fff}",
70
+ ".dasm_btnSolidDanger:hover:not(:disabled){background:var(--dsw-alias-state-error-primary-hover,#e04f4f);color:#fff}",
71
+ ".dasm_quarantine{border:.5px solid var(--dsw-alias-border-l2);border-radius:8px;align-items:center;gap:8px;padding:8px 10px;display:flex;flex-wrap:wrap}",
72
+ ".dasm_quarantineText{color:var(--dsw-alias-label-secondary,#b8bbc2);font-size:12px;line-height:18px;flex:1;min-width:180px}",
73
+ ].join("");
74
+
75
+ if (typeof document !== "undefined" && document.querySelector('style[data-plugin-css="' + STYLE_ID + '"]') === null) {
76
+ var tag = document.createElement("style");
77
+ tag.dataset.plugin = "dsh-archived";
78
+ tag.dataset.pluginCss = STYLE_ID;
79
+ tag.textContent = CSS;
80
+ document.head.appendChild(tag);
81
+ }
82
+
83
+ /** Same-origin call to this plugin's host routes; never throws on a JSON error body. */
84
+ function request(path, options) {
85
+ var init = {
86
+ method: options && options.method ? options.method : "GET",
87
+ credentials: "same-origin",
88
+ headers: { [HEADER]: "1" },
89
+ };
90
+ if (options && options.body !== undefined) {
91
+ init.headers["content-type"] = "application/json";
92
+ init.body = JSON.stringify(options.body);
93
+ }
94
+ return fetch(path, init).then(function (response) {
95
+ return response.json().catch(function () {
96
+ throw new Error("HTTP " + response.status);
97
+ });
98
+ });
99
+ }
100
+
101
+ function formatBytes(bytes) {
102
+ if (typeof bytes !== "number" || !isFinite(bytes) || bytes <= 0) return "0 KB";
103
+ if (bytes < 1024) return bytes + " B";
104
+ if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + " KB";
105
+ return (bytes / (1024 * 1024)).toFixed(1) + " MB";
106
+ }
107
+
108
+ /** Localized compact relative time of one row's last activity. */
109
+ function timeLabel(updatedAt, now, t) {
110
+ if (typeof updatedAt !== "number") return "";
111
+ var elapsed = Math.max(0, now - updatedAt);
112
+ var minutes = Math.floor(elapsed / 60000);
113
+ if (minutes < 1) return t("time.now");
114
+ if (minutes < 60) return t("time.minutes", { n: minutes });
115
+ var hours = Math.floor(minutes / 60);
116
+ if (hours < 24) return t("time.hours", { n: hours });
117
+ var days = Math.floor(hours / 24);
118
+ if (days < 30) return t("time.days", { n: days });
119
+ var months = Math.floor(days / 30);
120
+ if (months < 12) return t("time.months", { n: months });
121
+ return t("time.years", { n: Math.floor(months / 12) });
122
+ }
123
+
124
+ /** Last path segment of a cwd, used as the group heading. */
125
+ function baseName(path) {
126
+ if (typeof path !== "string" || path.length === 0) return "";
127
+ var parts = path.split(/[\\/]/).filter(function (part) {
128
+ return part.length > 0;
129
+ });
130
+ return parts.length > 0 ? parts[parts.length - 1] : path;
131
+ }
132
+
133
+ function matches(row, normalizedQuery) {
134
+ if (normalizedQuery.length === 0) return true;
135
+ return (
136
+ row.title.toLowerCase().indexOf(normalizedQuery) !== -1 ||
137
+ row.id.toLowerCase().indexOf(normalizedQuery) !== -1 ||
138
+ row.group.toLowerCase().indexOf(normalizedQuery) !== -1
139
+ );
140
+ }
141
+
142
+ /**
143
+ * Archived-sessions Settings page with permanent deletion.
144
+ *
145
+ * @param props - composed slot props: localizer, the injected unarchive
146
+ * action, and the framework's session/workspace selector seats.
147
+ */
148
+ function ArchivedSessionsSection(props) {
149
+ var t = props.t;
150
+ var unarchive = props.unarchive;
151
+ var useSessions = props.useSessions;
152
+ var useWorkspaces = props.useWorkspaces;
153
+
154
+ // The seats are optional: on a host that does not compose them the page
155
+ // still renders, because the rows come from the host route.
156
+ var summaries = useSessions
157
+ ? useSessions(function (state) {
158
+ return state.byId;
159
+ })
160
+ : {};
161
+ var archivedSessionIds = useWorkspaces
162
+ ? useWorkspaces(function (state) {
163
+ return state.archivedSessionIds;
164
+ })
165
+ : [];
166
+
167
+ var [snapshot, setSnapshot] = useState(null);
168
+ var [failed, setFailed] = useState(false);
169
+ var [query, setQuery] = useState("");
170
+ var [selected, setSelected] = useState({});
171
+ var [expanded, setExpanded] = useState({});
172
+ var [confirm, setConfirm] = useState(null);
173
+ var [confirmAll, setConfirmAll] = useState(null);
174
+ var [busy, setBusy] = useState(null);
175
+ var [notice, setNotice] = useState(null);
176
+
177
+ var idsKey = archivedSessionIds ? archivedSessionIds.join(",") : "";
178
+
179
+ function reload() {
180
+ return request(STATE_PATH)
181
+ .then(function (payload) {
182
+ if (!payload || payload.ok !== true) throw new Error("state");
183
+ setSnapshot(payload);
184
+ setFailed(false);
185
+ return payload;
186
+ })
187
+ .catch(function () {
188
+ setFailed(true);
189
+ setSnapshot(null);
190
+ });
191
+ }
192
+
193
+ useEffect(
194
+ function () {
195
+ reload();
196
+ },
197
+ [idsKey],
198
+ );
199
+
200
+ var now = Date.now();
201
+ var normalized = query.trim().toLowerCase();
202
+
203
+ var rows = useMemo(
204
+ function () {
205
+ var built = [];
206
+ var items = snapshot && Array.isArray(snapshot.items) ? snapshot.items : [];
207
+ for (var i = 0; i < items.length; i += 1) {
208
+ var item = items[i];
209
+ var summary = summaries ? summaries[item.id] : undefined;
210
+ var group = item.cwd ? baseName(item.cwd) : t("ungrouped");
211
+ built.push({
212
+ id: item.id,
213
+ title: (summary && summary.displayTitle) || item.title || item.id,
214
+ group: group,
215
+ updatedAt: summary ? summary.updatedAt : item.createdAt,
216
+ item: item,
217
+ });
218
+ }
219
+ built.sort(function (a, b) {
220
+ return (b.updatedAt || 0) - (a.updatedAt || 0);
221
+ });
222
+ return built;
223
+ },
224
+ [snapshot, summaries, t],
225
+ );
226
+
227
+ var visible = rows.filter(function (row) {
228
+ return matches(row, normalized);
229
+ });
230
+
231
+ var groups = useMemo(
232
+ function () {
233
+ var order = [];
234
+ var map = {};
235
+ for (var i = 0; i < visible.length; i += 1) {
236
+ var row = visible[i];
237
+ if (map[row.group] === undefined) {
238
+ map[row.group] = [];
239
+ order.push(row.group);
240
+ }
241
+ map[row.group].push(row);
242
+ }
243
+ return order.map(function (name) {
244
+ return { name: name, rows: map[name] };
245
+ });
246
+ },
247
+ [visible],
248
+ );
249
+
250
+ var archivedCount = rows.length;
251
+ var quarantine = (snapshot && snapshot.quarantine) || { count: 0, bytes: 0, items: [] };
252
+ var selectedIds = Object.keys(selected).filter(function (id) {
253
+ return selected[id] === true;
254
+ });
255
+
256
+ function describeError(code) {
257
+ var key = "error." + String(code);
258
+ var text = t(key);
259
+ return text === key ? t("error.generic") : text;
260
+ }
261
+
262
+ function run(path, body, onDone) {
263
+ setNotice(null);
264
+ return request(path, { method: "POST", body: body })
265
+ .then(function (payload) {
266
+ if (!payload || payload.ok === false) {
267
+ throw new Error(payload && payload.error === "busy" ? t("error.busy") : describeError(payload && payload.error));
268
+ }
269
+ setConfirm(null);
270
+ setConfirmAll(null);
271
+ if (onDone) onDone(payload);
272
+ return payload;
273
+ })
274
+ .catch(function (error) {
275
+ setNotice({ kind: "error", text: String(error && error.message ? error.message : error) });
276
+ })
277
+ .then(function () {
278
+ setBusy(null);
279
+ return reload();
280
+ });
281
+ }
282
+
283
+ function deleteRow(row, mode) {
284
+ setBusy(row.id);
285
+ run(DELETE_PATH, { sessionId: row.id, mode: mode }, function () {
286
+ setNotice({
287
+ kind: "ok",
288
+ text:
289
+ mode === "forever"
290
+ ? t("deletedForeverOne", { title: row.title })
291
+ : t("deletedOne", { title: row.title, size: formatBytes(row.item.artifact.bytes) }),
292
+ });
293
+ });
294
+ }
295
+
296
+ function deleteSelected(mode) {
297
+ var ids = selectedIds;
298
+ if (ids.length === 0) return;
299
+ setBusy("__selected__");
300
+ run(DELETE_ALL_PATH, { ids: ids, mode: mode }, function (payload) {
301
+ var ok = payload.results.filter(function (result) {
302
+ return result.ok === true;
303
+ }).length;
304
+ var bytes = 0;
305
+ for (var i = 0; i < payload.results.length; i += 1) {
306
+ if (payload.results[i].ok && payload.results[i].removed) bytes += payload.results[i].removed.bytes || 0;
307
+ }
308
+ setSelected({});
309
+ setNotice(
310
+ ok === payload.results.length
311
+ ? { kind: "ok", text: t("deletedMany", { n: ok, size: formatBytes(bytes) }) }
312
+ : { kind: "error", text: t("deletedPartial", { ok: ok, failed: payload.results.length - ok }) },
313
+ );
314
+ });
315
+ }
316
+
317
+ function deleteAll(mode) {
318
+ var ids = rows.map(function (row) {
319
+ return row.id;
320
+ });
321
+ setBusy("__all__");
322
+ run(DELETE_ALL_PATH, { ids: ids, mode: mode }, function (payload) {
323
+ var ok = payload.results.filter(function (result) {
324
+ return result.ok === true;
325
+ }).length;
326
+ var bytes = 0;
327
+ for (var i = 0; i < payload.results.length; i += 1) {
328
+ if (payload.results[i].ok && payload.results[i].removed) bytes += payload.results[i].removed.bytes || 0;
329
+ }
330
+ setNotice(
331
+ ok === payload.results.length
332
+ ? { kind: "ok", text: t("deletedMany", { n: ok, size: formatBytes(bytes) }) }
333
+ : { kind: "error", text: t("deletedPartial", { ok: ok, failed: payload.results.length - ok }) },
334
+ );
335
+ });
336
+ }
337
+
338
+ function restoreRow(entry) {
339
+ setBusy(entry.id);
340
+ run(RESTORE_PATH, { sessionId: entry.id }, function () {
341
+ setNotice({ kind: "ok", text: t("restoredOne", { title: entry.title || entry.id }) });
342
+ });
343
+ }
344
+
345
+ function emptyQuarantine() {
346
+ setBusy("__quarantine__");
347
+ run(EMPTY_PATH, {}, function (payload) {
348
+ setNotice({ kind: "ok", text: t("purgedQuarantine", { n: payload.purged, size: formatBytes(payload.bytes) }) });
349
+ });
350
+ }
351
+
352
+ function toggleSelected(id) {
353
+ setSelected(function (previous) {
354
+ var next = Object.assign({}, previous);
355
+ if (next[id] === true) delete next[id];
356
+ else next[id] = true;
357
+ return next;
358
+ });
359
+ }
360
+
361
+ function toggleExpanded(id) {
362
+ setExpanded(function (previous) {
363
+ var next = Object.assign({}, previous);
364
+ next[id] = next[id] !== true;
365
+ return next;
366
+ });
367
+ }
368
+
369
+ if (failed) {
370
+ return h("div", { className: "dasm_section" }, [
371
+ h("h2", { className: "dasm_title", key: "title" }, t("nav")),
372
+ h("p", { className: "dasm_status", key: "failed" }, t("hostUnavailable")),
373
+ ]);
374
+ }
375
+
376
+ if (snapshot === null) {
377
+ return h("div", { className: "dasm_section" }, h("p", { className: "dasm_status" }, t("loading")));
378
+ }
379
+
380
+ if (snapshot.apiVersion !== API_VERSION) {
381
+ return h("div", { className: "dasm_section" }, [
382
+ h("h2", { className: "dasm_title", key: "title" }, t("nav")),
383
+ h("p", { className: "dasm_status", key: "mismatch" }, t("apiMismatch")),
384
+ ]);
385
+ }
386
+
387
+ var children = [h("h2", { className: "dasm_title", key: "title" }, t("nav"))];
388
+
389
+ var head = h("div", { className: "dasm_head", key: "head" }, [
390
+ h(
391
+ "div",
392
+ { className: "dasm_search", key: "search" },
393
+ [
394
+ h(
395
+ "svg",
396
+ { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", key: "icon" },
397
+ [
398
+ h("circle", { cx: 7, cy: 7, r: 4.5, stroke: "currentColor", strokeWidth: 1.2, key: "c" }),
399
+ h("path", { d: "M10.5 10.5 14 14", stroke: "currentColor", strokeWidth: 1.2, key: "p" }),
400
+ ],
401
+ ),
402
+ h("input", {
403
+ type: "search",
404
+ value: query,
405
+ placeholder: t("search"),
406
+ "aria-label": t("search"),
407
+ onChange: function (event) {
408
+ setQuery(event.currentTarget.value);
409
+ },
410
+ key: "input",
411
+ }),
412
+ ],
413
+ ),
414
+ ]);
415
+ children.push(head);
416
+
417
+ var tools = [];
418
+ if (archivedCount > 0) {
419
+ tools.push(
420
+ h(
421
+ "button",
422
+ {
423
+ type: "button",
424
+ className: "dasm_btn",
425
+ disabled: busy !== null,
426
+ onClick: function () {
427
+ var next = {};
428
+ if (selectedIds.length !== archivedCount) {
429
+ for (var i = 0; i < rows.length; i += 1) next[rows[i].id] = true;
430
+ }
431
+ setSelected(next);
432
+ },
433
+ key: "selectAll",
434
+ },
435
+ selectedIds.length === archivedCount ? t("clearSelection") : t("selectAll"),
436
+ ),
437
+ );
438
+ }
439
+ if (selectedIds.length > 0) {
440
+ tools.push(
441
+ h(
442
+ "button",
443
+ {
444
+ type: "button",
445
+ className: "dasm_btn dasm_btnDanger",
446
+ disabled: busy !== null,
447
+ onClick: function () {
448
+ deleteSelected("quarantine");
449
+ },
450
+ key: "deleteSelected",
451
+ },
452
+ t("deleteSelected", { n: selectedIds.length }),
453
+ ),
454
+ );
455
+ }
456
+ if (archivedCount > 0) {
457
+ tools.push(
458
+ h(
459
+ "button",
460
+ {
461
+ type: "button",
462
+ className: "dasm_btn dasm_btnDanger",
463
+ disabled: busy !== null,
464
+ onClick: function () {
465
+ setNotice(null);
466
+ setConfirmAll("quarantine");
467
+ },
468
+ key: "deleteAll",
469
+ },
470
+ t("deleteAll", { n: archivedCount }),
471
+ ),
472
+ );
473
+ }
474
+ if (tools.length > 0) children.push(h("div", { className: "dasm_tools", key: "tools" }, tools));
475
+
476
+ if (confirmAll !== null) {
477
+ children.push(
478
+ h("div", { className: "dasm_quarantine", key: "confirmAll" }, [
479
+ h("span", { className: "dasm_quarantineText", key: "text" }, t("deleteAllHint", { n: archivedCount })),
480
+ h(
481
+ "button",
482
+ {
483
+ type: "button",
484
+ className: "dasm_btn dasm_btnSolidDanger",
485
+ disabled: busy !== null,
486
+ onClick: function () {
487
+ deleteAll(confirmAll);
488
+ },
489
+ key: "go",
490
+ },
491
+ t("deleteConfirmAction"),
492
+ ),
493
+ h(
494
+ "button",
495
+ {
496
+ type: "button",
497
+ className: "dasm_btn",
498
+ disabled: busy !== null,
499
+ onClick: function () {
500
+ setConfirmAll(null);
501
+ },
502
+ key: "cancel",
503
+ },
504
+ t("cancel"),
505
+ ),
506
+ ]),
507
+ );
508
+ }
509
+
510
+ if (quarantine.count > 0) {
511
+ children.push(
512
+ h("div", { className: "dasm_quarantine", key: "quarantine" }, [
513
+ h(
514
+ "span",
515
+ { className: "dasm_quarantineText", key: "text" },
516
+ t("quarantineSummary", { n: quarantine.count, size: formatBytes(quarantine.bytes) }),
517
+ ),
518
+ h(
519
+ "button",
520
+ {
521
+ type: "button",
522
+ className: "dasm_btn",
523
+ disabled: busy !== null,
524
+ onClick: function () {
525
+ restoreRow(quarantine.items[0]);
526
+ },
527
+ key: "restoreLatest",
528
+ },
529
+ t("restoreLatest"),
530
+ ),
531
+ h(
532
+ "button",
533
+ {
534
+ type: "button",
535
+ className: "dasm_btn dasm_btnDanger",
536
+ disabled: busy !== null,
537
+ onClick: emptyQuarantine,
538
+ key: "empty",
539
+ },
540
+ t("emptyQuarantine"),
541
+ ),
542
+ ]),
543
+ );
544
+ }
545
+
546
+ if (notice !== null) {
547
+ children.push(
548
+ h("p", { className: notice.kind === "error" ? "dasm_noticeError" : "dasm_notice", key: "notice" }, notice.text),
549
+ );
550
+ }
551
+
552
+ if (archivedCount === 0) {
553
+ children.push(h("p", { className: "dasm_status", key: "empty" }, t("empty")));
554
+ } else if (visible.length === 0) {
555
+ children.push(h("p", { className: "dasm_status", key: "emptySearch" }, t("emptySearch")));
556
+ }
557
+
558
+ for (var g = 0; g < groups.length; g += 1) {
559
+ var group = groups[g];
560
+ children.push(h("p", { className: "dasm_group", key: "group-" + group.name }, group.name));
561
+ children.push(
562
+ h(
563
+ "ul",
564
+ { className: "dasm_list", key: "list-" + group.name },
565
+ group.rows.map(function (row) {
566
+ var item = row.item;
567
+ var confirming = confirm !== null && confirm.id === row.id;
568
+ var isBusy = busy === row.id;
569
+ var meta = [row.group, timeLabel(row.updatedAt, now, t)];
570
+ if (item.artifact.present) meta.push(t("logSize", { size: formatBytes(item.artifact.bytes) }));
571
+ else if (item.cache) meta.push(t("cacheOnly"));
572
+ else meta.push(t("indexOnly"));
573
+ if (item.running) meta.push(t("running"));
574
+
575
+ var actions = [];
576
+ if (confirming) {
577
+ actions.push(h("span", { className: "dasm_hint", key: "hint" }, t("deleteHint")));
578
+ actions.push(
579
+ h(
580
+ "button",
581
+ {
582
+ type: "button",
583
+ className: "dasm_btn dasm_btnSolidDanger",
584
+ disabled: isBusy,
585
+ "aria-label": t("deleteNamed", { title: row.title }),
586
+ onClick: function () {
587
+ deleteRow(row, "quarantine");
588
+ },
589
+ key: "confirm",
590
+ },
591
+ t("deleteConfirmAction"),
592
+ ),
593
+ );
594
+ actions.push(
595
+ h(
596
+ "button",
597
+ {
598
+ type: "button",
599
+ className: "dasm_btn dasm_btnDanger",
600
+ disabled: isBusy,
601
+ onClick: function () {
602
+ deleteRow(row, "forever");
603
+ },
604
+ key: "forever",
605
+ },
606
+ t("deleteForever"),
607
+ ),
608
+ );
609
+ actions.push(
610
+ h(
611
+ "button",
612
+ {
613
+ type: "button",
614
+ className: "dasm_btn",
615
+ disabled: isBusy,
616
+ onClick: function () {
617
+ setConfirm(null);
618
+ },
619
+ key: "cancel",
620
+ },
621
+ t("cancel"),
622
+ ),
623
+ );
624
+ } else {
625
+ actions.push(
626
+ h(
627
+ "button",
628
+ {
629
+ type: "button",
630
+ className: "dasm_btn",
631
+ disabled: busy !== null,
632
+ "aria-label": t("unarchiveNamed", { title: row.title }),
633
+ onClick: function () {
634
+ unarchive(row.id).catch(function (reason) {
635
+ console.warn("session unarchive rejected:", reason);
636
+ });
637
+ },
638
+ key: "unarchive",
639
+ },
640
+ t("unarchive"),
641
+ ),
642
+ );
643
+ actions.push(
644
+ h(
645
+ "button",
646
+ {
647
+ type: "button",
648
+ className: "dasm_btn",
649
+ disabled: busy !== null,
650
+ "aria-label": t("revealNamed", { title: row.title }),
651
+ onClick: function () {
652
+ request(REVEAL_PATH, { method: "POST", body: { sessionId: row.id } }).catch(function () {});
653
+ },
654
+ key: "reveal",
655
+ },
656
+ t("reveal"),
657
+ ),
658
+ );
659
+ actions.push(
660
+ h(
661
+ "button",
662
+ {
663
+ type: "button",
664
+ className: "dasm_btn",
665
+ disabled: busy !== null,
666
+ "aria-label": t("detailsNamed", { title: row.title }),
667
+ onClick: function () {
668
+ toggleExpanded(row.id);
669
+ },
670
+ key: "details",
671
+ },
672
+ t("details"),
673
+ ),
674
+ );
675
+ actions.push(
676
+ h(
677
+ "button",
678
+ {
679
+ type: "button",
680
+ className: "dasm_btn dasm_btnDanger",
681
+ disabled: busy !== null,
682
+ "aria-label": t("deleteNamed", { title: row.title }),
683
+ onClick: function () {
684
+ setNotice(null);
685
+ setConfirmAll(null);
686
+ setConfirm({ id: row.id });
687
+ },
688
+ key: "delete",
689
+ },
690
+ t("delete"),
691
+ ),
692
+ );
693
+ }
694
+
695
+ var detail = null;
696
+ if (expanded[row.id] === true) {
697
+ var lines = [
698
+ t("detailId", { id: row.id }),
699
+ t("detailCreated", { time: timeLabel(item.createdAt, now, t) }),
700
+ item.artifact.present
701
+ ? t("detailPath", { path: item.artifact.path })
702
+ : t("detailNoArtifact"),
703
+ t("detailCache", { state: item.cache ? t("yes") : t("no") }),
704
+ ];
705
+ if (item.ancestors.length > 0) lines.push(t("detailParent", { ids: item.ancestors.join(", ") }));
706
+ if (item.children.length > 0) lines.push(t("detailChildren", { n: item.children.length }));
707
+ if (item.legacyRow) lines.push(t("detailLegacy"));
708
+ detail = h("span", { className: "dasm_detail", key: "detail" }, lines.join("\n"));
709
+ }
710
+
711
+ return h("li", { className: "dasm_row", key: row.id }, [
712
+ h("input", {
713
+ type: "checkbox",
714
+ className: "dasm_check",
715
+ checked: selected[row.id] === true,
716
+ disabled: busy !== null,
717
+ "aria-label": t("selectNamed", { title: row.title }),
718
+ onChange: function () {
719
+ toggleSelected(row.id);
720
+ },
721
+ key: "check",
722
+ }),
723
+ h("span", { className: "dasm_identity", key: "identity" }, [
724
+ h("span", { className: "dasm_title", key: "title" }, row.title),
725
+ h(
726
+ "span",
727
+ { className: "dasm_meta", key: "meta" },
728
+ meta
729
+ .filter(function (part) {
730
+ return typeof part === "string" && part.length > 0;
731
+ })
732
+ .join(" · "),
733
+ ),
734
+ detail,
735
+ ]),
736
+ h("div", { className: "dasm_actions", key: "actions" }, actions),
737
+ ]);
738
+ }),
739
+ ),
740
+ );
741
+ }
742
+
743
+ return h("div", { className: "dasm_section" }, children);
744
+ }
745
+
746
+ /** Dictionary namespace owned by this plugin. */
747
+ var NS = "dsh-archived";
748
+
749
+ /** Simplified Chinese dictionary and key source of truth. */
750
+ var zh = {
751
+ nav: "已归档",
752
+ search: "搜索已归档会话",
753
+ loading: "正在读取会话…",
754
+ hostUnavailable: "读不到宿主状态:请确认插件已随宿主加载,然后刷新页面。",
755
+ apiMismatch: "插件的前后端版本不一致:请重启 DSH 后再试。",
756
+ empty: "暂无已归档会话。",
757
+ emptySearch: "没有匹配的会话。",
758
+ ungrouped: "未分组",
759
+ unarchive: "取消归档",
760
+ unarchiveNamed: "取消归档 {title}",
761
+ delete: "删除",
762
+ deleteNamed: "删除 {title}",
763
+ deleteForever: "永久删除",
764
+ deleteHint: "删除后可在回收站恢复,永久删除不可恢复",
765
+ deleteConfirmAction: "确认删除",
766
+ cancel: "取消",
767
+ deleteSelected: "删除所选({n})",
768
+ deleteAll: "删除全部({n})",
769
+ deleteAllHint: "将把全部 {n} 个已归档会话移入回收站",
770
+ selectAll: "全选",
771
+ clearSelection: "取消全选",
772
+ selectNamed: "选择 {title}",
773
+ details: "详情",
774
+ detailsNamed: "查看 {title} 的详情",
775
+ reveal: "打开文件夹",
776
+ revealNamed: "打开 {title} 的文件夹",
777
+ quarantineSummary: "回收站:{n} 个 · {size},30 天后自动清除",
778
+ restoreLatest: "恢复最近一个",
779
+ emptyQuarantine: "清空回收站",
780
+ deletedOne: "已删除「{title}」,释放 {size}",
781
+ deletedForeverOne: "已永久删除「{title}」",
782
+ deletedMany: "已删除 {n} 个会话,释放 {size}",
783
+ deletedPartial: "已删除 {ok} 个,{failed} 个失败",
784
+ restoredOne: "已恢复「{title}」",
785
+ purgedQuarantine: "已清空回收站,释放 {size}({n} 个)",
786
+ indexOnly: "仅索引残留",
787
+ cacheOnly: "仅缓存",
788
+ logSize: "日志 {size}",
789
+ running: "运行中",
790
+ yes: "有",
791
+ no: "无",
792
+ detailId: "ID:{id}",
793
+ detailCreated: "创建:{time}",
794
+ detailPath: "位置:{path}",
795
+ detailNoArtifact: "位置:磁盘上已无日志目录",
796
+ detailCache: "投影缓存:{state}",
797
+ detailParent: "父会话:{ids}",
798
+ detailChildren: "子会话:{n} 个在内存中",
799
+ detailLegacy: "该 id 仍留在旧版聚合缓存里(历史遗留,宿主不再写入)",
800
+ "error.invalid-session-id": "会话 ID 无效。",
801
+ "error.not-archived": "该会话已不在归档列表中。",
802
+ "error.session-running": "会话正在运行中,未删除。",
803
+ "error.subagent-running": "它的子会话正在运行,未删除。",
804
+ "error.no-registry": "工作区注册表不可用,已拒绝删除。",
805
+ "error.busy": "该会话正被另一个操作占用,请稍后重试。",
806
+ "error.not-quarantined": "该会话不在回收站里。",
807
+ "error.no-artifact": "磁盘上找不到这个会话的目录。",
808
+ "error.forbidden": "请求被拒绝。",
809
+ "error.generic": "操作失败,请查看 DSH 日志。",
810
+ "time.now": "刚刚",
811
+ "time.minutes": "{n}分钟",
812
+ "time.hours": "{n}小时",
813
+ "time.days": "{n}天",
814
+ "time.months": "{n}个月",
815
+ "time.years": "{n}年",
816
+ };
817
+
818
+ /** English dictionary checked against the Chinese key set. */
819
+ var en = {
820
+ nav: "Archived",
821
+ search: "Search archived sessions",
822
+ loading: "Reading sessions…",
823
+ hostUnavailable: "Cannot read host state — check that the plugin loaded, then refresh.",
824
+ apiMismatch: "The plugin's two halves disagree on the API version — restart DSH and try again.",
825
+ empty: "No archived sessions.",
826
+ emptySearch: "No matching sessions.",
827
+ ungrouped: "Ungrouped",
828
+ unarchive: "Unarchive",
829
+ unarchiveNamed: "Unarchive {title}",
830
+ delete: "Delete",
831
+ deleteNamed: "Delete {title}",
832
+ deleteForever: "Delete forever",
833
+ deleteHint: "Delete keeps it recoverable; delete forever does not",
834
+ deleteConfirmAction: "Delete",
835
+ cancel: "Cancel",
836
+ deleteSelected: "Delete selected ({n})",
837
+ deleteAll: "Delete all ({n})",
838
+ deleteAllHint: "Moves all {n} archived sessions to the recycle bin",
839
+ selectAll: "Select all",
840
+ clearSelection: "Clear selection",
841
+ selectNamed: "Select {title}",
842
+ details: "Details",
843
+ detailsNamed: "Show details for {title}",
844
+ reveal: "Open folder",
845
+ revealNamed: "Open the folder for {title}",
846
+ quarantineSummary: "Recycle bin: {n} · {size}, cleared automatically after 30 days",
847
+ restoreLatest: "Restore latest",
848
+ emptyQuarantine: "Empty recycle bin",
849
+ deletedOne: "Deleted “{title}”, freed {size}",
850
+ deletedForeverOne: "Deleted “{title}” forever",
851
+ deletedMany: "Deleted {n} sessions, freed {size}",
852
+ deletedPartial: "Deleted {ok}, {failed} failed",
853
+ restoredOne: "Restored “{title}”",
854
+ purgedQuarantine: "Emptied the recycle bin, freed {size} ({n})",
855
+ indexOnly: "index only",
856
+ cacheOnly: "cache only",
857
+ logSize: "log {size}",
858
+ running: "running",
859
+ yes: "yes",
860
+ no: "no",
861
+ detailId: "ID: {id}",
862
+ detailCreated: "Created: {time}",
863
+ detailPath: "Path: {path}",
864
+ detailNoArtifact: "Path: no log directory on disk",
865
+ detailCache: "Projection cache: {state}",
866
+ detailParent: "Parent session: {ids}",
867
+ detailChildren: "Child sessions: {n} in memory",
868
+ detailLegacy: "This id is still in the legacy aggregate cache (host no longer writes it)",
869
+ "error.invalid-session-id": "Invalid session id.",
870
+ "error.not-archived": "That session is no longer archived.",
871
+ "error.session-running": "Session is running; not deleted.",
872
+ "error.subagent-running": "A subagent session of it is running; not deleted.",
873
+ "error.no-registry": "Workspace registry unavailable; deletion refused.",
874
+ "error.busy": "Another operation is already working on that session.",
875
+ "error.not-quarantined": "That session is not in the recycle bin.",
876
+ "error.no-artifact": "No directory for that session on disk.",
877
+ "error.forbidden": "Request rejected.",
878
+ "error.generic": "Failed — check the DSH log.",
879
+ "time.now": "now",
880
+ "time.minutes": "{n}min",
881
+ "time.hours": "{n}h",
882
+ "time.days": "{n}d",
883
+ "time.months": "{n}mo",
884
+ "time.years": "{n}y",
885
+ };
886
+
887
+ /** Services required by the Settings registration and the archive write. */
888
+ var inject = ["slots", "locale", "uiWorkspace"];
889
+
890
+ /** Contribute the archived-session page. */
891
+ function apply(ctx) {
892
+ ctx.effect(
893
+ function () {
894
+ return ctx.locale.register(NS, { zh: zh, en: en });
895
+ },
896
+ "dsh-archived: dictionaries",
897
+ );
898
+ var t = ctx.locale.bind(NS);
899
+ var injected = function () {
900
+ return {
901
+ unarchive: function (sessionId) {
902
+ return ctx.uiWorkspace.unarchiveSession(sessionId);
903
+ },
904
+ };
905
+ };
906
+ ctx.slots.inject("settings.section", function () {
907
+ return ctx.slots.register(
908
+ {
909
+ name: "settings.section",
910
+ id: "archived-sessions",
911
+ // Lowest priority wins the cell. On 0.2.0-rc.2 nothing else claims
912
+ // it; on an older host this still shadows the shipped page.
913
+ priority: -1,
914
+ order: 25,
915
+ label: function () {
916
+ return t("nav");
917
+ },
918
+ locale: NS,
919
+ inject: injected,
920
+ },
921
+ ArchivedSessionsSection,
922
+ );
923
+ });
924
+ }
925
+
926
+ exports.apply = apply;
927
+ exports.inject = inject;
928
+ exports.__test = { zh: zh, en: en };
929
+ return module.exports;
930
+ },
931
+ });