dsh-manage-session 0.1.0-alpha.0 → 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/lib/client.js CHANGED
@@ -26,13 +26,13 @@ window.__ModuleLoader__.load({
26
26
  "card.enabled": "启用会话管家",
27
27
  "card.enabled.desc": "关闭后不注册任何接口,侧边栏入口整体消失。",
28
28
  "card.history": "会话历史文件",
29
- "card.history.desc": "归档浏览与清理(病案室)。关闭后不注册历史接口、不渲染该页签。",
29
+ "card.history.desc": "归档浏览与清理(养老院)。关闭后不注册历史接口、不渲染该页签。",
30
30
  "card.health": "健康检查",
31
31
  "card.health.desc": "体检 → 处方 → 出院。关闭后不注册体检接口、不渲染该页签。",
32
32
  "card.readonly": "当前会话无法修改设置(只读挂载)。",
33
33
  "card.unavailable": "设置服务不可用,无法读取配置。",
34
34
  "panel.title": "会话管家",
35
- "panel.tab.history": "病案室",
35
+ "panel.tab.history": "养老院",
36
36
  "panel.tab.health": "体检",
37
37
  "panel.close": "关闭",
38
38
  "panel.untitled": "(无标题)",
@@ -42,20 +42,32 @@ window.__ModuleLoader__.load({
42
42
  "history.done": "完成",
43
43
  "history.selectAll": "全选",
44
44
  "history.unselectAll": "取消全选",
45
- "history.delete": "删除选中 ({n})",
46
- "history.deleting": "删除中…",
47
- "history.editingHint": "编辑模式:勾选要清理的会话,按「删除选中」从官方归档数组移除(自动备份,需重启 DSH 生效)。",
48
- "history.restartHint": "已从归档数组移除 {removed} 个 id(剩余 {remaining})。请重启 DSH 使官方侧生效。",
45
+ "history.unarchive": "取消归档状态 ({n})",
46
+ "history.unarchiving": "取消归档中…",
47
+ "history.purge": "清理归档文件 ({n}) · 释放 {size}",
48
+ "history.purging": "清理中…",
49
+ "history.editingHint": "编辑模式:勾选会话后二选一 ——「取消归档状态」只把它从官方归档数组移除(可逆,重启后回到侧边栏);「清理归档文件」真删磁盘上的转录与投影缓存(不可逆)。",
50
+ "history.restartHint": "已从归档数组移除 {removed} 个 id(剩余 {remaining})。请**立刻重启 DSH**:宿主退出前任何归档或工作区改动都会把整份内存快照写回文件,这次操作会被还原。",
51
+ "history.pendingRestart": "宿主内存里仍有 {n} 个归档未失效:列表已按存储文件显示(这些已不在),但它们对应的会话在重启 DSH 前仍被侧边栏隐藏。",
52
+ "history.count": "共 {n} 条 · 占 {size}",
53
+ "history.size": "转录 {t} · 缓存 {c}",
54
+ "history.sizeUnknown": "磁盘上无实体",
49
55
  "history.source.registry": "来源:宿主归档注册表",
50
56
  "history.source.storage-file": "来源:存储文件",
51
57
  "history.source.none": "两处都读不到归档集合",
52
- "history.confirm": "确认从归档数组中移除 {n} 个会话 id?\n\n{summary}\n\n将写入存储文件(自动备份),需要重启 DSH 后官方侧生效。此操作不可撤销。",
58
+ "history.confirmUnarchive": "确认取消这 {n} 个会话的归档状态?\n\n{summary}\n\n只改归档数组(自动备份),磁盘文件不动。重启 DSH 后这些会话回到原工作区。",
59
+ "history.confirmPurge": "确认清理这 {n} 个归档会话的磁盘文件?\n\n{summary}\n\n合计释放约 {size}。\n将删除:转录目录(含旧格式副本)、逐条投影缓存,并从归档数组与工作区成员表中移除。\n\n**此操作不可逆,会话将无法恢复。**",
60
+ "history.purgeResult": "已清理 {purged} 个归档会话,释放 {size}。请重启 DSH 使其彻底消失。",
61
+ "history.purgePartial": "已清理 {purged} 个,释放 {size};{failed} 个失败(见下方明细)。",
62
+ "history.purgeFailures": "失败明细",
53
63
  "health.scan": "开始体检",
54
64
  "health.scanning": "体检中…",
65
+ "health.progress": "{done}/{total} · 已用 {sec}s",
55
66
  "health.empty": "未发现异常会话。",
56
67
  "health.clean": "四门全绿。",
57
68
  "health.level.ok": "正常",
58
69
  "health.level.warn": "注意",
70
+ "health.level.skipped": "未检查",
59
71
  "health.level.fail": "异常",
60
72
  "health.gate.log-integrity": "日志完整性",
61
73
  "health.gate.projection-cache": "投影缓存",
@@ -71,11 +83,19 @@ window.__ModuleLoader__.load({
71
83
  "health.repairing": "处置中…",
72
84
  "health.before": "处置前",
73
85
  "health.after": "处置后",
86
+ "health.quarantined": "已隔离投影缓存记录(重启 DSH 后重折叠)",
74
87
  "health.commands": "可执行命令",
75
88
  "health.copy": "复制",
76
89
  "health.copied": "已复制",
77
90
  "health.detail": "查看详情",
78
- "health.back": "返回列表"
91
+ "health.back": "返回列表",
92
+ "health.cache.hint": "结果来自缓存 · {ago}",
93
+ "health.cache.refresh": "刷新",
94
+ "health.cache.corpusChanged": "语料已变化({was} → {now}),建议刷新",
95
+ "health.ago.justNow": "刚刚",
96
+ "health.ago.minutes": "{n} 分钟前",
97
+ "health.ago.hours": "{n} 小时前",
98
+ "health.ago.days": "{n} 天前"
79
99
  };
80
100
  /** English dictionary (same keys). */
81
101
  const en = {
@@ -84,13 +104,13 @@ window.__ModuleLoader__.load({
84
104
  "card.enabled": "Enable Session Steward",
85
105
  "card.enabled.desc": "When off, no API is registered and the sidebar entry disappears.",
86
106
  "card.history": "Session history files",
87
- "card.history.desc": "Archive browsing and cleanup (Records Room). When off, history routes and the tab are gone.",
107
+ "card.history.desc": "Archive browsing and cleanup (Retirement Home). When off, history routes and the tab are gone.",
88
108
  "card.health": "Health check",
89
109
  "card.health.desc": "Checkup → Prescription → Discharge. When off, health routes and the tab are gone.",
90
110
  "card.readonly": "This session cannot edit settings (read-only mount).",
91
111
  "card.unavailable": "Settings service unavailable; configuration cannot be read.",
92
112
  "panel.title": "Session Steward",
93
- "panel.tab.history": "Records Room",
113
+ "panel.tab.history": "Retirement Home",
94
114
  "panel.tab.health": "Checkup",
95
115
  "panel.close": "Close",
96
116
  "panel.untitled": "(untitled)",
@@ -100,20 +120,32 @@ window.__ModuleLoader__.load({
100
120
  "history.done": "Done",
101
121
  "history.selectAll": "Select all",
102
122
  "history.unselectAll": "Clear selection",
103
- "history.delete": "Delete selected ({n})",
104
- "history.deleting": "Deleting…",
105
- "history.editingHint": "Edit mode: tick sessions to clean, then remove them from the official archive array (backed up; a DSH restart is required).",
106
- "history.restartHint": "Removed {removed} id(s) from the archive array ({remaining} left). Restart DSH to apply.",
123
+ "history.unarchive": "Unarchive state ({n})",
124
+ "history.unarchiving": "Unarchiving…",
125
+ "history.purge": "Purge archive files ({n}) · free {size}",
126
+ "history.purging": "Purging…",
127
+ "history.editingHint": "Edit mode: tick sessions, then pick one — \"Unarchive state\" only removes them from the official archive array (reversible; they return to the sidebar after a restart); \"Purge archive files\" really deletes the transcript and projection cache from disk (irreversible).",
128
+ "history.restartHint": "Removed {removed} id(s) from the archive array ({remaining} left). **Restart DSH now**: until the host exits, any archive or workspace change rewrites the whole in-memory snapshot back to the file and undoes this.",
129
+ "history.pendingRestart": "{n} archived id(s) are still live in host memory: the list follows the storage file (they are already gone), but their sessions stay hidden from the sidebar until DSH restarts.",
130
+ "history.count": "{n} total · {size} on disk",
131
+ "history.size": "transcript {t} · cache {c}",
132
+ "history.sizeUnknown": "no on-disk entity",
107
133
  "history.source.registry": "Source: host archive registry",
108
134
  "history.source.storage-file": "Source: storage file",
109
135
  "history.source.none": "The archive set is unreadable from both sources",
110
- "history.confirm": "Remove {n} session id(s) from the archive array?\n\n{summary}\n\nThis writes the storage file (auto backup) and needs a DSH restart. It cannot be undone.",
136
+ "history.confirmUnarchive": "Unarchive these {n} session(s)?\n\n{summary}\n\nOnly the archive array is edited (auto backup); nothing on disk is touched. After a DSH restart these sessions return to their workspaces.",
137
+ "history.confirmPurge": "Purge the on-disk files of these {n} archived session(s)?\n\n{summary}\n\nAbout {size} will be freed.\nThis deletes: the transcript directory (including older-format copies) and the per-session projection cache, and removes them from the archive array and the workspace membership tables.\n\n**This cannot be undone — the sessions will be unrecoverable.**",
138
+ "history.purgeResult": "Purged {purged} archived session(s), freed {size}. Restart DSH to make them disappear completely.",
139
+ "history.purgePartial": "Purged {purged}, freed {size}; {failed} failed (see details below).",
140
+ "history.purgeFailures": "Failure details",
111
141
  "health.scan": "Run checkup",
112
142
  "health.scanning": "Running…",
143
+ "health.progress": "{done}/{total} · {sec}s elapsed",
113
144
  "health.empty": "No problem session found.",
114
145
  "health.clean": "All four gates are green.",
115
146
  "health.level.ok": "ok",
116
147
  "health.level.warn": "warn",
148
+ "health.level.skipped": "Not checked",
117
149
  "health.level.fail": "fail",
118
150
  "health.gate.log-integrity": "Log integrity",
119
151
  "health.gate.projection-cache": "Projection cache",
@@ -129,11 +161,19 @@ window.__ModuleLoader__.load({
129
161
  "health.repairing": "Repairing…",
130
162
  "health.before": "Before",
131
163
  "health.after": "After",
164
+ "health.quarantined": "Projection cache quarantined (refolds after a DSH restart)",
132
165
  "health.commands": "Commands",
133
166
  "health.copy": "Copy",
134
167
  "health.copied": "Copied",
135
168
  "health.detail": "Details",
136
- "health.back": "Back"
169
+ "health.back": "Back",
170
+ "health.cache.hint": "Cached result · {ago}",
171
+ "health.cache.refresh": "Refresh",
172
+ "health.cache.corpusChanged": "Corpus changed ({was} → {now}); refresh recommended",
173
+ "health.ago.justNow": "just now",
174
+ "health.ago.minutes": "{n} min ago",
175
+ "health.ago.hours": "{n} h ago",
176
+ "health.ago.days": "{n} d ago"
137
177
  };
138
178
  /** 字典查找(缺 key 时回退到内置 zh,再回退 key 本身)。 */
139
179
  function translate(t, key, params) {
@@ -150,9 +190,18 @@ window.__ModuleLoader__.load({
150
190
  //#endregion
151
191
  //#region src/client/card.tsx
152
192
  /**
153
- * 设置卡(插件配置行):两个开关 —— 会话历史文件 / 健康检查。
193
+ * 设置卡(插件配置行):一个可折叠抽屉 —— 会话历史文件 / 健康检查。
194
+ *
195
+ * **chrome 必须自己渲染**:DSH 的「插件」设置分区(`dsh-client-ui-settings-plugins`)
196
+ * 只做一件事——按 settings 命名空间分发 `settings.plugin.item`:
197
+ *
198
+ * renderSlot('settings.plugin.item', {}, { entryKey: ns })
199
+ *
200
+ * 它**不提供任何外壳**。所以卡片的标题、说明、展开/收起全归插件自己所有
201
+ * (同分区里 search-index 等卡片都是这个形态)。早期版本只渲染了裸行,
202
+ * 结果既与分区内其它插件的抽屉形态不一致,又让 `card.title` / `card.desc`
203
+ * 变成定义了却没人渲染的死文案。
154
204
  *
155
- * 与 dsh-session-search-toggle 的 card 同范式:`Row`(标题+说明+控件)+ `Toggle`,
156
205
  * 读写走 settingsScope 的显式 `set`(用户在控件上的直接操作,非隐式提交)。
157
206
  */
158
207
  /** 一行设置项。 */
@@ -183,14 +232,52 @@ window.__ModuleLoader__.load({
183
232
  className: "dss_toggleTrack"
184
233
  }, (0, react.createElement)("span", { className: "dss_toggleKnob" }))]);
185
234
  }
186
- /** 插件设置卡主体。 */
235
+ /** 抽屉头:标题 + 说明 + chevron,整块可点。 */
236
+ function Header(props) {
237
+ return (0, react.createElement)("button", {
238
+ key: "head",
239
+ type: "button",
240
+ className: "dss_setHead",
241
+ "aria-expanded": props.open,
242
+ onClick: props.onToggle
243
+ }, [(0, react.createElement)("span", {
244
+ key: "text",
245
+ className: "dss_setHeadText"
246
+ }, [(0, react.createElement)("span", {
247
+ key: "t",
248
+ className: "dss_setCardTitle"
249
+ }, translate(props.t, "card.title")), (0, react.createElement)("span", {
250
+ key: "d",
251
+ className: "dss_setCardDesc"
252
+ }, translate(props.t, "card.desc"))]), (0, react.createElement)("svg", {
253
+ key: "chev",
254
+ className: "dss_setChev",
255
+ width: 16,
256
+ height: 16,
257
+ viewBox: "0 0 16 16",
258
+ "aria-hidden": true
259
+ }, (0, react.createElement)("path", {
260
+ d: "M4 6l4 4 4-4",
261
+ fill: "none",
262
+ stroke: "currentColor",
263
+ strokeWidth: 1.5,
264
+ strokeLinecap: "round",
265
+ strokeLinejoin: "round"
266
+ }))]);
267
+ }
268
+ /** 插件设置卡主体(抽屉)。默认收起,与「插件」分区内其它卡片一致。 */
187
269
  function StewardSettingsCard({ t, scope }) {
188
270
  const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
271
+ const [open, setOpen] = (0, react.useState)(false);
189
272
  const value = snapshot.value ?? {};
190
273
  const writable = snapshot.writable;
191
- if (snapshot.status === "unavailable") return (0, react.createElement)("div", { className: "dss_setRow" }, (0, react.createElement)("span", { className: "dss_setTitle" }, translate(t, "card.unavailable")));
192
- const rows = [
193
- (0, react.createElement)(Row, {
274
+ const rows = [];
275
+ if (snapshot.status === "unavailable") rows.push((0, react.createElement)("div", {
276
+ key: "unavailable",
277
+ className: "dss_setDesc"
278
+ }, translate(t, "card.unavailable")));
279
+ else {
280
+ rows.push((0, react.createElement)(Row, {
194
281
  key: "enabled",
195
282
  title: translate(t, "card.enabled"),
196
283
  desc: translate(t, "card.enabled.desc"),
@@ -201,8 +288,7 @@ window.__ModuleLoader__.load({
201
288
  scope.set("enabled", checked);
202
289
  }
203
290
  })
204
- }),
205
- (0, react.createElement)(Row, {
291
+ }), (0, react.createElement)(Row, {
206
292
  key: "history",
207
293
  title: translate(t, "card.history"),
208
294
  desc: translate(t, "card.history.desc"),
@@ -213,8 +299,7 @@ window.__ModuleLoader__.load({
213
299
  scope.set("historyFiles", checked);
214
300
  }
215
301
  })
216
- }),
217
- (0, react.createElement)(Row, {
302
+ }), (0, react.createElement)(Row, {
218
303
  key: "health",
219
304
  title: translate(t, "card.health"),
220
305
  desc: translate(t, "card.health.desc"),
@@ -225,13 +310,23 @@ window.__ModuleLoader__.load({
225
310
  scope.set("healthCheck", checked);
226
311
  }
227
312
  })
228
- })
229
- ];
230
- if (!writable) rows.push((0, react.createElement)("div", {
231
- key: "ro",
232
- className: "dss_setDesc"
233
- }, translate(t, "card.readonly")));
234
- return (0, react.createElement)("div", { className: "dss_setBody" }, rows);
313
+ }));
314
+ if (!writable) rows.push((0, react.createElement)("div", {
315
+ key: "ro",
316
+ className: "dss_setDesc"
317
+ }, translate(t, "card.readonly")));
318
+ }
319
+ return (0, react.createElement)("div", { className: `dss_setCard${open ? " dss_setCardOpen" : ""}` }, [(0, react.createElement)(Header, {
320
+ key: "head",
321
+ t,
322
+ open,
323
+ onToggle: () => {
324
+ setOpen((current) => !current);
325
+ }
326
+ }), open && (0, react.createElement)("div", {
327
+ key: "body",
328
+ className: "dss_setBody"
329
+ }, rows)]);
235
330
  }
236
331
  //#endregion
237
332
  //#region src/client/host-api.ts
@@ -263,23 +358,51 @@ window.__ModuleLoader__.load({
263
358
  //#endregion
264
359
  //#region src/client/history-panel.tsx
265
360
  /**
266
- * 病案室(会话历史文件)面板 —— 自 dsh-session-search-toggle `src/client/archive-panel.tsx`
267
- * **迁入**并适配到本包的方法名(`session-history-list` / `session-history-prune`)。
361
+ * 养老院(会话历史文件)面板 —— 自 dsh-session-search-toggle `src/client/archive-panel.tsx`
362
+ * **迁入**并适配到本包的方法名。
363
+ *
364
+ * 与迁移前的关键差异(**两个操作刻意分开,不可互换**):
365
+ *
366
+ * | 操作 | 路由 | 改什么 | 可逆 |
367
+ * |---|---|---|---|
368
+ * | 取消归档状态 | `session-history-prune` | 只改归档数组 | ✅ 重启后会话回到侧边栏 |
369
+ * | 清理归档文件 | `session-history-purge` | **真删**转录目录 + 投影缓存 + 两处 id | ❌ 不可逆 |
268
370
  *
269
- * 行为与迁移前一致(含「显式编辑模式才允许清理」的门禁与 JS confirm):
270
- * 行不可导航(归档会话已离开活跃系统);清理写存储文件(自动备份)并需要重启 DSH。
271
- * 差异:列表来源改为官方归档集合真值,因此额外显示来源与降级提示。
371
+ * 「删除」一词在本面板**不存在** —— 旧文案承诺「删除」却只改了个数组,
372
+ * 是导致「点了确认没反应」那类误判的一部分。
373
+ * 体积**按行**显示(实测单条可从 0 到 23 MB,报一个总量没有意义),按钮上给所选小计。
272
374
  */
273
- /** 病案室面板。 */
375
+ /** 人类可读体积。 */
376
+ function fmtSize(bytes) {
377
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
378
+ const units = [
379
+ "B",
380
+ "KB",
381
+ "MB",
382
+ "GB",
383
+ "TB"
384
+ ];
385
+ let value = bytes;
386
+ let unit = 0;
387
+ while (value >= 1024 && unit < units.length - 1) {
388
+ value /= 1024;
389
+ unit++;
390
+ }
391
+ return `${unit === 0 || value >= 100 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
392
+ }
393
+ /** 养老院面板。 */
274
394
  function HistoryPanel({ t, onClose }) {
275
395
  const [items, setItems] = (0, react.useState)(null);
276
396
  const [source, setSource] = (0, react.useState)("");
277
397
  const [degraded, setDegraded] = (0, react.useState)("");
398
+ const [pendingRestart, setPendingRestart] = (0, react.useState)(0);
278
399
  const [error, setError] = (0, react.useState)(null);
279
400
  const [attempt, setAttempt] = (0, react.useState)(0);
280
401
  const [selected, setSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
281
- const [pruning, setPruning] = (0, react.useState)(false);
402
+ const [busy, setBusy] = (0, react.useState)(null);
282
403
  const [note, setNote] = (0, react.useState)(null);
404
+ /** 清理失败明细(逐条);空数组表示无失败。 */
405
+ const [failures, setFailures] = (0, react.useState)([]);
283
406
  const [editing, setEditing] = (0, react.useState)(false);
284
407
  (0, react.useEffect)(() => {
285
408
  let cancelled = false;
@@ -290,6 +413,7 @@ window.__ModuleLoader__.load({
290
413
  setItems(res.items);
291
414
  setSource(typeof res.source === "string" ? res.source : "");
292
415
  setDegraded(typeof res.degraded === "string" ? res.degraded : "");
416
+ setPendingRestart(typeof res.pendingRestart === "number" ? res.pendingRestart : 0);
293
417
  } else setError(res.error ?? "读取归档列表失败");
294
418
  });
295
419
  return () => {
@@ -318,31 +442,69 @@ window.__ModuleLoader__.load({
318
442
  setEditing((prev) => !prev);
319
443
  setSelected(/* @__PURE__ */ new Set());
320
444
  setNote(null);
445
+ setFailures([]);
321
446
  };
322
447
  const toggleAll = () => {
323
448
  setSelected((prev) => prev.size === (items?.length ?? 0) ? /* @__PURE__ */ new Set() : new Set((items ?? []).map((item) => item.sessionId)));
324
449
  };
325
- /** JS confirm 门禁,然后提交清理。 */
326
- const pruneSelected = () => {
327
- const ids = [...selected];
328
- if (ids.length === 0) return;
329
- const summary = ids.length <= 5 ? ids.map((id) => `${id.slice(0, 22)}…`).join("\n") : `${ids.slice(0, 4).map((id) => `${id.slice(0, 22)}…`).join("\n")}\n… 共 ${ids.length} 个`;
330
- if (!(typeof window !== "undefined" && typeof window.confirm === "function" ? window.confirm(translate(t, "history.confirm", {
331
- n: ids.length,
332
- summary
333
- })) : false)) return;
334
- setPruning(true);
450
+ const selectedRows = (items ?? []).filter((item) => selected.has(item.sessionId));
451
+ const selectedBytes = selectedRows.reduce((sum, item) => sum + item.bytes + item.cacheBytes, 0);
452
+ const totalBytes = (items ?? []).reduce((sum, item) => sum + item.bytes + item.cacheBytes, 0);
453
+ /** 确认弹窗里的逐条摘要(清理时带体积,取消归档时不带)。 */
454
+ const summarise = (rows, withSize) => {
455
+ const lines = rows.slice(0, 5).map((row) => {
456
+ const label = row.title || translate(t, "panel.untitled");
457
+ const size = fmtSize(row.bytes + row.cacheBytes);
458
+ return withSize ? `· ${label} — ${size}` : `· ${label}`;
459
+ });
460
+ if (rows.length > 5) lines.push(`… 共 ${rows.length} 个`);
461
+ return lines.join("\n");
462
+ };
463
+ const confirm = (message) => typeof window !== "undefined" && typeof window.confirm === "function" ? window.confirm(message) : false;
464
+ /** 提交一次操作:确认 → 调用 → 刷新列表(列表以存储文件为准,因此立刻可见变化)。 */
465
+ const run = (kind, method, confirmText) => {
466
+ const rows = selectedRows;
467
+ if (rows.length === 0) return;
468
+ if (!confirm(confirmText)) return;
469
+ setBusy(kind);
335
470
  setNote(null);
336
- callHostAny("session-history-prune", { sessionIds: ids }, 6e4).then((res) => {
471
+ setFailures([]);
472
+ callHostAny(method, { sessionIds: rows.map((row) => row.sessionId) }, 6e4).then((res) => {
337
473
  if (res.ok === true) {
338
- setNote(translate(t, "history.restartHint", {
339
- removed: res.removed ?? ids.length,
474
+ if (kind === "unarchive") setNote(translate(t, "history.restartHint", {
475
+ removed: res.removed ?? rows.length,
340
476
  remaining: res.remaining ?? "?"
341
477
  }));
478
+ else {
479
+ const freed = fmtSize(res.freedBytes ?? 0);
480
+ const failed = res.failures?.length ?? 0;
481
+ setNote(failed === 0 ? translate(t, "history.purgeResult", {
482
+ purged: res.purged ?? rows.length,
483
+ size: freed
484
+ }) : translate(t, "history.purgePartial", {
485
+ purged: res.purged ?? rows.length,
486
+ size: freed,
487
+ failed
488
+ }));
489
+ setFailures(res.failures ?? []);
490
+ }
342
491
  setSelected(/* @__PURE__ */ new Set());
343
492
  setAttempt((n) => n + 1);
344
- } else setNote(res.error ?? "清理失败");
345
- }).catch((err) => setNote(`清理失败:${String(err instanceof Error ? err.message : err)}`)).finally(() => setPruning(false));
493
+ } else setNote(res.error ?? "操作失败");
494
+ }).catch((err) => setNote(`操作失败:${String(err instanceof Error ? err.message : err)}`)).finally(() => setBusy(null));
495
+ };
496
+ const unarchiveSelected = () => {
497
+ run("unarchive", "session-history-prune", translate(t, "history.confirmUnarchive", {
498
+ n: selectedRows.length,
499
+ summary: summarise(selectedRows, false)
500
+ }));
501
+ };
502
+ const purgeSelected = () => {
503
+ run("purge", "session-history-purge", translate(t, "history.confirmPurge", {
504
+ n: selectedRows.length,
505
+ summary: summarise(selectedRows, true),
506
+ size: fmtSize(selectedBytes)
507
+ }));
346
508
  };
347
509
  const children = [];
348
510
  if (error !== null) children.push((0, react.createElement)("div", {
@@ -371,18 +533,31 @@ window.__ModuleLoader__.load({
371
533
  key: "manage",
372
534
  className: "dss_btnRow",
373
535
  style: { padding: "4px 10px 0" }
374
- }, [(0, react.createElement)("button", {
375
- key: "all",
376
- type: "button",
377
- className: "dss_actBtn",
378
- onClick: toggleAll
379
- }, allSelected ? translate(t, "history.unselectAll") : translate(t, "history.selectAll")), (0, react.createElement)("button", {
380
- key: "prune",
381
- type: "button",
382
- className: "dss_actBtn dss_dangerBtn",
383
- disabled: pruning || selected.size === 0,
384
- onClick: pruneSelected
385
- }, pruning ? translate(t, "history.deleting") : translate(t, "history.delete", { n: selected.size }))]));
536
+ }, [
537
+ (0, react.createElement)("button", {
538
+ key: "all",
539
+ type: "button",
540
+ className: "dss_actBtn",
541
+ onClick: toggleAll
542
+ }, allSelected ? translate(t, "history.unselectAll") : translate(t, "history.selectAll")),
543
+ (0, react.createElement)("button", {
544
+ key: "unarchive",
545
+ type: "button",
546
+ className: "dss_actBtn",
547
+ disabled: busy !== null || selected.size === 0,
548
+ onClick: unarchiveSelected
549
+ }, busy === "unarchive" ? translate(t, "history.unarchiving") : translate(t, "history.unarchive", { n: selected.size })),
550
+ (0, react.createElement)("button", {
551
+ key: "purge",
552
+ type: "button",
553
+ className: "dss_actBtn dss_dangerBtn",
554
+ disabled: busy !== null || selected.size === 0,
555
+ onClick: purgeSelected
556
+ }, busy === "purge" ? translate(t, "history.purging") : translate(t, "history.purge", {
557
+ n: selected.size,
558
+ size: fmtSize(selectedBytes)
559
+ }))
560
+ ]));
386
561
  children.push((0, react.createElement)("ul", {
387
562
  key: "list",
388
563
  className: "dss_list",
@@ -412,6 +587,13 @@ window.__ModuleLoader__.load({
412
587
  key: "tag",
413
588
  className: "dss_tag"
414
589
  }, fmtTime(item.updatedAt))]),
590
+ (0, react.createElement)("span", {
591
+ key: "size",
592
+ className: "dss_meta dss_size"
593
+ }, item.bytes + item.cacheBytes > 0 ? translate(t, "history.size", {
594
+ t: fmtSize(item.bytes),
595
+ c: fmtSize(item.cacheBytes)
596
+ }) : translate(t, "history.sizeUnknown")),
415
597
  item.cwd !== "" && (0, react.createElement)("span", {
416
598
  key: "c",
417
599
  className: "dss_meta"
@@ -427,6 +609,13 @@ window.__ModuleLoader__.load({
427
609
  key: "note",
428
610
  className: "dss_status"
429
611
  }, note),
612
+ failures.length > 0 && (0, react.createElement)("div", {
613
+ key: "fail",
614
+ className: "dss_status dss_error"
615
+ }, [(0, react.createElement)("div", { key: "h" }, translate(t, "history.purgeFailures")), ...failures.slice(0, 10).map((failure, index) => (0, react.createElement)("div", {
616
+ key: `f${index}`,
617
+ className: "dss_meta"
618
+ }, `${failure.sessionId.slice(0, 22)}… — ${failure.reason}`))]),
430
619
  editing && (0, react.createElement)("div", {
431
620
  key: "hint",
432
621
  className: "dss_status"
@@ -435,10 +624,21 @@ window.__ModuleLoader__.load({
435
624
  key: "degraded",
436
625
  className: "dss_status dss_warnText"
437
626
  }, degraded),
627
+ pendingRestart > 0 && (0, react.createElement)("div", {
628
+ key: "pending",
629
+ className: "dss_status dss_warnText"
630
+ }, translate(t, "history.pendingRestart", { n: pendingRestart })),
438
631
  source !== "" && (0, react.createElement)("div", {
439
632
  key: "source",
440
633
  className: "dss_metaLine"
441
634
  }, translate(t, source === "registry" ? "history.source.registry" : source === "storage-file" ? "history.source.storage-file" : "history.source.none")),
635
+ items !== null && error === null && (0, react.createElement)("div", {
636
+ key: "count",
637
+ className: "dss_metaLine"
638
+ }, translate(t, "history.count", {
639
+ n: items.length,
640
+ size: fmtSize(totalBytes)
641
+ })),
442
642
  (0, react.createElement)("div", {
443
643
  key: "headBtns",
444
644
  className: "dss_btnRow"
@@ -483,8 +683,44 @@ window.__ModuleLoader__.load({
483
683
  const LEVEL_LABEL = {
484
684
  ok: "health.level.ok",
485
685
  warn: "health.level.warn",
686
+ skipped: "health.level.skipped",
486
687
  fail: "health.level.fail"
487
688
  };
689
+ /**
690
+ * 单批扫描的会话数。
691
+ *
692
+ * 宿主侧 `scanSessions` 是同步循环,一次批太大就长时间占住事件循环、界面全无反馈。
693
+ * 小批次连续调用把控制权交回客户端:每批之间有真实的进度与计时,且宿主无需持有
694
+ * 跨请求状态(见 host/health/scan.ts 的 offset 语义)。
695
+ */
696
+ const SCAN_BATCH = 5;
697
+ /**
698
+ * 把缓存生成时刻折成一个粗粒度的相对时间 key。
699
+ *
700
+ * 不引第三方 i18n/时间库:四档粒度足够表达「这是刚才的 / 是昨天的」,
701
+ * 而这正是用户判断「要不要刷新」所需的全部信息。
702
+ */
703
+ function relativeAgo(generatedAt, now) {
704
+ const seconds = Math.max(0, Math.round((now - generatedAt) / 1e3));
705
+ if (seconds < 60) return {
706
+ key: "health.ago.justNow",
707
+ n: 0
708
+ };
709
+ const minutes = Math.floor(seconds / 60);
710
+ if (minutes < 60) return {
711
+ key: "health.ago.minutes",
712
+ n: minutes
713
+ };
714
+ const hours = Math.floor(minutes / 60);
715
+ if (hours < 24) return {
716
+ key: "health.ago.hours",
717
+ n: hours
718
+ };
719
+ return {
720
+ key: "health.ago.days",
721
+ n: Math.floor(hours / 24)
722
+ };
723
+ }
488
724
  /** 体检面板。 */
489
725
  function HealthPanel({ t, onClose }) {
490
726
  const [scanning, setScanning] = (0, react.useState)(false);
@@ -494,6 +730,14 @@ window.__ModuleLoader__.load({
494
730
  const [repairing, setRepairing] = (0, react.useState)(false);
495
731
  const [discharge, setDischarge] = (0, react.useState)(null);
496
732
  const [copied, setCopied] = (0, react.useState)(null);
733
+ /** 分批扫描进度:done = 已访问会话数,total = 语料总数(0 表示尚未拿到分母)。 */
734
+ const [progress, setProgress] = (0, react.useState)(null);
735
+ const [startedAt, setStartedAt] = (0, react.useState)(0);
736
+ const [elapsedMs, setElapsedMs] = (0, react.useState)(0);
737
+ /** 缓存命中时的元信息;非 null 即「当前列表来自缓存」,必须露出生成时间。 */
738
+ const [cacheInfo, setCacheInfo] = (0, react.useState)(null);
739
+ /** 相对时间的走字刻度:缓存提示里的「N 分钟前」需要自己更新。 */
740
+ const [, setClockTick] = (0, react.useState)(0);
497
741
  (0, react.useEffect)(() => {
498
742
  const onKey = (event) => {
499
743
  if (event.key === "Escape") onClose();
@@ -503,25 +747,115 @@ window.__ModuleLoader__.load({
503
747
  document.removeEventListener("keydown", onKey);
504
748
  };
505
749
  }, [onClose]);
506
- const runScan = () => {
750
+ (0, react.useEffect)(() => {
751
+ callHostAny("session-health-scan", { resume: true }, 3e4).then((res) => {
752
+ if (res.ok === true && res.cached === true) {
753
+ setFindings(res.findings ?? []);
754
+ setCacheInfo({
755
+ generatedAt: res.generatedAt ?? 0,
756
+ total: res.total ?? 0,
757
+ currentTotal: res.currentTotal ?? res.total ?? 0
758
+ });
759
+ }
760
+ });
761
+ }, []);
762
+ (0, react.useEffect)(() => {
763
+ if (cacheInfo === null) return;
764
+ const timer = setInterval(() => {
765
+ setClockTick((tick) => tick + 1);
766
+ }, 3e4);
767
+ return () => {
768
+ clearInterval(timer);
769
+ };
770
+ }, [cacheInfo]);
771
+ (0, react.useEffect)(() => {
772
+ if (!scanning || startedAt === 0) return;
773
+ const timer = setInterval(() => {
774
+ setElapsedMs(Date.now() - startedAt);
775
+ }, 100);
776
+ return () => {
777
+ clearInterval(timer);
778
+ };
779
+ }, [scanning, startedAt]);
780
+ const runScan = (options) => {
781
+ const forceRefresh = options?.forceRefresh === true;
507
782
  setScanning(true);
508
783
  setError(null);
509
784
  setDetail(null);
510
785
  setDischarge(null);
511
- callHostAny("session-health-scan", {
512
- limit: 30,
513
- onlyProblems: true
514
- }, 12e4).then((res) => {
515
- if (res.ok === true) setFindings(res.findings ?? []);
516
- else setError(res.error ?? "体检失败");
517
- }).catch((err) => setError(String(err instanceof Error ? err.message : err))).finally(() => setScanning(false));
786
+ setFindings(null);
787
+ setCacheInfo(null);
788
+ setProgress({
789
+ done: 0,
790
+ total: 0
791
+ });
792
+ setStartedAt(Date.now());
793
+ setElapsedMs(0);
794
+ (async () => {
795
+ const collected = [];
796
+ let offset = 0;
797
+ try {
798
+ for (;;) {
799
+ const res = await callHostAny("session-health-scan", {
800
+ limit: SCAN_BATCH,
801
+ offset,
802
+ onlyProblems: true,
803
+ ...offset === 0 && !forceRefresh ? { resume: true } : {}
804
+ }, 12e4);
805
+ if (res.ok !== true) {
806
+ setError(res.error ?? "体检失败");
807
+ return;
808
+ }
809
+ if (res.cached === true) {
810
+ setFindings(res.findings ?? []);
811
+ setCacheInfo({
812
+ generatedAt: res.generatedAt ?? 0,
813
+ total: res.total ?? 0,
814
+ currentTotal: res.currentTotal ?? res.total ?? 0
815
+ });
816
+ setProgress(null);
817
+ return;
818
+ }
819
+ const total = res.total ?? 0;
820
+ const scanned = res.scanned ?? 0;
821
+ collected.push(...res.findings ?? []);
822
+ offset += scanned;
823
+ setProgress({
824
+ done: offset,
825
+ total
826
+ });
827
+ if (scanned === 0 || offset >= total) break;
828
+ }
829
+ setFindings(collected);
830
+ } catch (err) {
831
+ setError(String(err instanceof Error ? err.message : err));
832
+ } finally {
833
+ setScanning(false);
834
+ }
835
+ })();
836
+ };
837
+ /**
838
+ * 把单会话的最新报告同步进列表。
839
+ *
840
+ * 宿主侧缓存已就地回写(见 index.ts 的 `cache?.patch`),客户端列表必须同步,
841
+ * 否则退回列表时会看到已被处置的行还挂着旧档位。
842
+ */
843
+ const reconcileRow = (next) => {
844
+ setFindings((prev) => {
845
+ if (prev === null) return prev;
846
+ const index = prev.findIndex((item) => item.sessionId === next.sessionId);
847
+ if (next.level === "ok") return index < 0 ? prev : prev.filter((item) => item.sessionId !== next.sessionId);
848
+ return index < 0 ? [...prev, next] : prev.map((item) => item.sessionId === next.sessionId ? next : item);
849
+ });
518
850
  };
519
851
  const openDetail = (sessionId) => {
520
852
  setError(null);
521
853
  setDischarge(null);
522
854
  callHostAny("session-health-session", { sessionId }, 12e4).then((res) => {
523
- if (res.ok === true) setDetail(res);
524
- else setError(res.error ?? "读取体检详情失败");
855
+ if (res.ok === true) {
856
+ setDetail(res);
857
+ if (res.report !== void 0) reconcileRow(res.report);
858
+ } else setError(res.error ?? "读取体检详情失败");
525
859
  }).catch((err) => setError(String(err instanceof Error ? err.message : err)));
526
860
  };
527
861
  const runRepair = (sessionId) => {
@@ -530,11 +864,14 @@ window.__ModuleLoader__.load({
530
864
  callHostAny("session-health-repair", { sessionId }, 12e4).then((res) => {
531
865
  if (res.ok === true) {
532
866
  setDischarge(res);
533
- if (res.after !== void 0) setDetail({
534
- ok: true,
535
- report: res.after,
536
- prescriptions: res.prescriptions
537
- });
867
+ if (res.after !== void 0) {
868
+ setDetail({
869
+ ok: true,
870
+ report: res.after,
871
+ prescriptions: res.prescriptions
872
+ });
873
+ reconcileRow(res.after);
874
+ }
538
875
  } else setError(res.error ?? "处置失败");
539
876
  }).catch((err) => setError(String(err instanceof Error ? err.message : err))).finally(() => setRepairing(false));
540
877
  };
@@ -559,6 +896,55 @@ window.__ModuleLoader__.load({
559
896
  disabled: scanning,
560
897
  onClick: runScan
561
898
  }, scanning ? translate(t, "health.scanning") : translate(t, "health.scan"))]));
899
+ if (scanning && progress !== null) {
900
+ const total = progress.total;
901
+ const pct = total > 0 ? Math.min(100, Math.round(progress.done / total * 100)) : 0;
902
+ lines.push((0, react.createElement)("div", {
903
+ key: "progress",
904
+ className: "dss_progressRow"
905
+ }, [(0, react.createElement)("div", {
906
+ key: "track",
907
+ className: "dss_progressTrack"
908
+ }, (0, react.createElement)("div", {
909
+ key: "fill",
910
+ className: total > 0 ? "dss_progressFill" : "dss_progressFill dss_progressIndeterminate",
911
+ ...total > 0 ? { style: { width: `${pct}%` } } : {}
912
+ })), (0, react.createElement)("span", {
913
+ key: "read",
914
+ className: "dss_meta"
915
+ }, translate(t, "health.progress", {
916
+ done: progress.done,
917
+ total: total > 0 ? total : "?",
918
+ sec: (elapsedMs / 1e3).toFixed(1)
919
+ }))]));
920
+ }
921
+ if (cacheInfo !== null && !scanning && detail === null && discharge === null) {
922
+ const ago = relativeAgo(cacheInfo.generatedAt, Date.now());
923
+ lines.push((0, react.createElement)("div", {
924
+ key: "cache",
925
+ className: "dss_cacheRow"
926
+ }, [
927
+ (0, react.createElement)("span", {
928
+ key: "h",
929
+ className: "dss_meta"
930
+ }, translate(t, "health.cache.hint", { ago: translate(t, ago.key, { n: ago.n }) })),
931
+ cacheInfo.currentTotal !== cacheInfo.total && (0, react.createElement)("span", {
932
+ key: "chg",
933
+ className: "dss_why"
934
+ }, translate(t, "health.cache.corpusChanged", {
935
+ was: cacheInfo.total,
936
+ now: cacheInfo.currentTotal
937
+ })),
938
+ (0, react.createElement)("button", {
939
+ key: "r",
940
+ type: "button",
941
+ className: "dss_actBtn",
942
+ onClick: () => {
943
+ runScan({ forceRefresh: true });
944
+ }
945
+ }, translate(t, "health.cache.refresh"))
946
+ ]));
947
+ }
562
948
  if (error !== null) lines.push((0, react.createElement)("div", {
563
949
  key: "err",
564
950
  className: "dss_error"
@@ -608,27 +994,34 @@ window.__ModuleLoader__.load({
608
994
  key: "list",
609
995
  className: "dss_list",
610
996
  role: "list"
611
- }, findings.map((item) => (0, react.createElement)("li", {
612
- key: item.sessionId,
613
- className: "dss_row"
614
- }, [
615
- (0, react.createElement)("span", {
616
- key: "lv",
617
- className: `dss_levelPill dss_level_${item.level}`
618
- }, translate(t, LEVEL_LABEL[item.level])),
619
- (0, react.createElement)("span", {
620
- key: "id",
621
- className: "dss_meta dss_uuid"
622
- }, item.sessionId),
623
- (0, react.createElement)("button", {
624
- key: "d",
625
- type: "button",
626
- className: "dss_actBtn",
627
- onClick: () => {
628
- openDetail(item.sessionId);
629
- }
630
- }, translate(t, "health.detail"))
631
- ]))));
997
+ }, findings.map((item) => {
998
+ const reasons = item.gates.filter((gate) => gate.level !== "ok").map((gate) => `${translate(t, GATE_LABEL[gate.id])}·${translate(t, LEVEL_LABEL[gate.level])}`).join(" · ");
999
+ return (0, react.createElement)("li", {
1000
+ key: item.sessionId,
1001
+ className: "dss_row"
1002
+ }, [
1003
+ (0, react.createElement)("span", {
1004
+ key: "lv",
1005
+ className: `dss_levelPill dss_level_${item.level}`
1006
+ }, translate(t, LEVEL_LABEL[item.level])),
1007
+ (0, react.createElement)("span", {
1008
+ key: "id",
1009
+ className: "dss_meta dss_uuid"
1010
+ }, item.sessionId),
1011
+ reasons !== "" && (0, react.createElement)("span", {
1012
+ key: "why",
1013
+ className: "dss_why"
1014
+ }, reasons),
1015
+ (0, react.createElement)("button", {
1016
+ key: "d",
1017
+ type: "button",
1018
+ className: "dss_actBtn",
1019
+ onClick: () => {
1020
+ openDetail(item.sessionId);
1021
+ }
1022
+ }, translate(t, "health.detail"))
1023
+ ]);
1024
+ })));
632
1025
  }
633
1026
  if (report !== void 0) {
634
1027
  lines.push((0, react.createElement)("div", {
@@ -645,7 +1038,11 @@ window.__ModuleLoader__.load({
645
1038
  onClick: () => {
646
1039
  runRepair(report.sessionId);
647
1040
  }
648
- }, repairing ? translate(t, "health.repairing") : translate(t, "health.repair"))]));
1041
+ }, [repairing ? (0, react.createElement)("span", {
1042
+ key: "sp",
1043
+ className: "dss_spinner",
1044
+ "aria-hidden": "true"
1045
+ }) : null, (0, react.createElement)("span", { key: "tx" }, repairing ? translate(t, "health.repairing") : translate(t, "health.repair"))])]));
649
1046
  const prescriptions = detail?.prescriptions ?? discharge?.prescriptions ?? [];
650
1047
  if (prescriptions.length > 0) lines.push((0, react.createElement)("div", {
651
1048
  key: "cmds",
@@ -686,10 +1083,14 @@ window.__ModuleLoader__.load({
686
1083
  className: `dss_levelPill dss_level_${discharge.after?.level ?? "warn"}`
687
1084
  }, `${translate(t, "health.after")}: ${translate(t, LEVEL_LABEL[discharge.after?.level ?? "warn"])}`)
688
1085
  ]));
1086
+ if (discharge.explanation !== void 0 && discharge.explanation !== "") lines.push((0, react.createElement)("div", {
1087
+ key: "verdict",
1088
+ className: `dss_verdict dss_verdict_${discharge.verdict ?? "failed"}`
1089
+ }, discharge.explanation));
689
1090
  if (discharge.repair?.ok === true) lines.push((0, react.createElement)("div", {
690
1091
  key: "repairNote",
691
1092
  className: "dss_status"
692
- }, `已隔离投影缓存记录 → ${discharge.repair.to ?? ""}(重启 DSH 后重折叠)`));
1093
+ }, `${translate(t, "health.quarantined")} → ${discharge.repair.to ?? ""}`));
693
1094
  else if (discharge.repair?.error !== void 0) lines.push((0, react.createElement)("div", {
694
1095
  key: "repairErr",
695
1096
  className: "dss_status"
@@ -700,9 +1101,9 @@ window.__ModuleLoader__.load({
700
1101
  //#endregion
701
1102
  //#region src/client/panel.tsx
702
1103
  /**
703
- * 会话管家面板壳:一个侧边栏入口,两个页签(病案室 / 体检)。
1104
+ * 会话管家面板壳:一个侧边栏入口,两个页签(养老院 / 体检)。
704
1105
  *
705
- * 页签可见性由开关决定:`historyFiles=false` 时「病案室」消失,`healthCheck=false`
1106
+ * 页签可见性由开关决定:`historyFiles=false` 时「养老院」消失,`healthCheck=false`
706
1107
  * 时「体检」消失;两者都关时进入面板直接显示空壳提示(客户端也会隐藏入口)。
707
1108
  */
708
1109
  /** 页签 id(导出供测试断言)。 */
@@ -785,18 +1186,23 @@ window.__ModuleLoader__.load({
785
1186
  }, translate(t, "panel.close"))
786
1187
  ]), body])]), document.body);
787
1188
  }
788
- /** 侧边栏脚部入口(照抄 toggle 的 footer 组件形态)。 */
1189
+ /** 侧边栏脚部入口:宽栏带文案成行控件,收起轨道退化为 36x36 图标钮。 */
789
1190
  function StewardFooter(props) {
1191
+ const wide = props.wide === true;
790
1192
  return (0, react.createElement)("button", {
791
1193
  type: "button",
792
- className: "dss_footerEntry",
793
- title: props.title ?? "会话管家",
794
- "aria-label": props.title ?? "会话管家",
1194
+ className: wide ? "dss_footerEntry" : "dss_footerEntry dss_footerEntryRail",
1195
+ title: props.title,
1196
+ "aria-label": props.title,
795
1197
  onClick: props.onClick
796
- }, (0, react.createElement)("span", {
1198
+ }, [(0, react.createElement)("span", {
1199
+ key: "icon",
797
1200
  className: "dss_footerIcon",
798
1201
  "aria-hidden": true
799
- }, "🧭"));
1202
+ }, "🧭"), wide && props.label !== void 0 ? (0, react.createElement)("span", {
1203
+ key: "label",
1204
+ className: "dss_footerLabel"
1205
+ }, props.label) : null]);
800
1206
  }
801
1207
  //#endregion
802
1208
  //#region src/client/index.ts
@@ -812,10 +1218,13 @@ window.__ModuleLoader__.load({
812
1218
  const style = document.createElement("style");
813
1219
  style.id = id;
814
1220
  style.textContent = `
815
- .dss_entryWrap{display:inline-flex;align-items:center}
816
- .dss_footerEntry{width:28px;height:28px;border:none;background:transparent;cursor:pointer;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-secondary)}
1221
+ .dss_entryWrap{flex:0 1 auto;display:inline-flex;align-items:center;min-width:0}
1222
+ .dss_entryWrapWide{margin-left:6px}
1223
+ .dss_footerEntry{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:4px;height:42px;padding:0 10px 0 8px;border:none;border-radius:12px;background:transparent;cursor:pointer;color:var(--dsw-alias-label-primary);font-family:inherit;font-size:14px;line-height:22px;white-space:nowrap;overflow:hidden;transition:background-color 160ms ease-out,color 160ms ease-out}
817
1224
  .dss_footerEntry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
818
- .dss_footerIcon{font-size:15px;line-height:1}
1225
+ .dss_footerEntryRail{width:28px;height:28px;padding:0;gap:0;border-radius:50%}
1226
+ .dss_footerIcon{flex:none;font-size:15px;line-height:1}
1227
+ .dss_footerLabel{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
819
1228
  .dss_backdrop{position:fixed;inset:0;background:rgba(15,20,30,.42);z-index:1000}
820
1229
  .dss_panel{position:fixed;z-index:1001;left:50%;top:8vh;transform:translateX(-50%);width:min(760px,92vw);max-height:78vh;overflow:auto;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:14px;box-shadow:0 18px 48px rgba(0,0,0,.18);padding:10px 14px 14px}
821
1230
  .dss_dialogHead{display:flex;align-items:center;gap:10px;min-height:34px}
@@ -838,6 +1247,8 @@ window.__ModuleLoader__.load({
838
1247
  .dss_meta{font-size:11px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
839
1248
  .dss_metaLine{font-size:11px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
840
1249
  .dss_uuid{font-family:var(--ds-font-family-code,monospace)}
1250
+ /* 按行的磁盘占用:右对齐、不参与标题压缩,方便纵向比对「哪条最占地方」。 */
1251
+ .dss_size{flex:none;font-variant-numeric:tabular-nums;white-space:nowrap}
841
1252
  .dss_check{display:inline-flex;align-items:center}
842
1253
  .dss_status{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280)}
843
1254
  .dss_warnText{color:var(--dsw-alias-state-warning-primary,#d97706)}
@@ -851,12 +1262,36 @@ window.__ModuleLoader__.load({
851
1262
  .dss_level_ok{background:rgba(22,163,74,.12);color:#15803d}
852
1263
  .dss_level_warn{background:rgba(217,119,6,.14);color:#b45309}
853
1264
  .dss_level_fail{background:rgba(220,38,38,.14);color:#b91c1c}
1265
+ .dss_level_skipped{background:rgba(107,114,128,.14);color:#4b5563}
1266
+ .dss_progressRow{display:flex;align-items:center;gap:8px;margin-top:2px}
1267
+ .dss_progressTrack{flex:1;height:6px;border-radius:999px;background:var(--dsw-alias-bg-layer-1,#eef0f3);overflow:hidden}
1268
+ .dss_progressFill{height:100%;border-radius:999px;background:var(--dsw-alias-state-business-primary,#3d5be0);transition:width .2s ease}
1269
+ .dss_progressIndeterminate{width:35%;animation:dssSlide 1.1s ease-in-out infinite}
1270
+ .dss_why{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);flex:1;min-width:0}
1271
+ .dss_cacheRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px}
1272
+ .dss_spinner{width:11px;height:11px;margin-right:6px;border-radius:50%;display:inline-block;vertical-align:-1px;border:2px solid currentColor;border-top-color:transparent;animation:dssSpin .7s linear infinite}
1273
+ @keyframes dssSpin{to{transform:rotate(360deg)}}
1274
+ @keyframes dssSlide{0%{margin-left:-35%}100%{margin-left:100%}}
1275
+ @media (prefers-reduced-motion:reduce){.dss_spinner{animation:none}.dss_progressIndeterminate{animation:none;width:100%;opacity:.5}.dss_setChev{transition:none}.dss_setCard{transition:none}}
1276
+ .dss_verdict{font-size:12px;line-height:18px;border-radius:8px;padding:6px 10px;margin-top:6px;border:1px solid transparent}
1277
+ .dss_verdict_repaired{background:rgba(22,163,74,.10);color:#15803d;border-color:rgba(22,163,74,.28)}
1278
+ .dss_verdict_repaired-with-residual{background:rgba(217,119,6,.10);color:#b45309;border-color:rgba(217,119,6,.28)}
1279
+ .dss_verdict_nothing-to-do{background:rgba(107,114,128,.10);color:#4b5563;border-color:rgba(107,114,128,.28)}
1280
+ .dss_verdict_not-applicable{background:rgba(107,114,128,.10);color:#4b5563;border-color:rgba(107,114,128,.28)}
1281
+ .dss_verdict_failed{background:rgba(220,38,38,.10);color:#b91c1c;border-color:rgba(220,38,38,.28)}
854
1282
  .dss_phaseRow{display:flex;gap:8px;align-items:center}
855
1283
  .dss_phase{font-weight:600;font-size:12px}
856
1284
  .dss_cmdBlock{display:flex;flex-direction:column;gap:4px;border:1px dashed var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;padding:6px 8px}
857
1285
  .dss_cmdLine{display:flex;gap:6px;align-items:center}
858
1286
  .dss_cmd{flex:1 1 auto;font-family:var(--ds-font-family-code,monospace);font-size:11px;white-space:pre-wrap;word-break:break-all}
859
- .dss_setBody{display:flex;flex-direction:column;gap:2px}
1287
+ .dss_setCard{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-layer-3,rgba(127,127,127,.05));border-radius:12px;transition:border-color .16s,background .16s}
1288
+ .dss_setHead{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:none;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}
1289
+ .dss_setHeadText{display:flex;flex-direction:column;gap:2px;flex:1 1 0%;min-width:0}
1290
+ .dss_setCardTitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary,#111827)}
1291
+ .dss_setCardDesc{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#9ca3af)}
1292
+ .dss_setChev{color:var(--dsw-alias-label-tertiary,#9ca3af);flex:0 0 auto;transition:transform .16s}
1293
+ .dss_setCardOpen .dss_setChev{transform:rotate(180deg)}
1294
+ .dss_setBody{display:flex;flex-direction:column;gap:2px;padding:0 16px 12px}
860
1295
  .dss_setRow{display:flex;align-items:center;gap:10px;padding:6px 0}
861
1296
  .dss_setText{display:flex;flex-direction:column;flex:1 1 auto;min-width:0}
862
1297
  .dss_setTitle{font-size:13px;color:var(--dsw-alias-label-primary,#111827)}
@@ -922,10 +1357,15 @@ window.__ModuleLoader__.load({
922
1357
  };
923
1358
  const historyFiles = config.historyFiles !== false;
924
1359
  const healthCheck = config.healthCheck !== false;
925
- if (!(config.enabled !== false && (historyFiles || healthCheck))) return (0, react.createElement)("span", { className: "dss_entryWrap" });
926
- return (0, react.createElement)("span", { className: "dss_entryWrap" }, [(0, react.createElement)(StewardFooter, {
1360
+ const visible = config.enabled !== false && (historyFiles || healthCheck);
1361
+ const wide = props.wide === true;
1362
+ const label = translate(void 0, "panel.title");
1363
+ if (!visible) return (0, react.createElement)("span", { className: "dss_entryWrap" });
1364
+ return (0, react.createElement)("span", { className: wide ? "dss_entryWrap dss_entryWrapWide" : "dss_entryWrap" }, [(0, react.createElement)(StewardFooter, {
927
1365
  key: "btn",
928
- title: translate(void 0, "panel.title"),
1366
+ title: label,
1367
+ label,
1368
+ wide,
929
1369
  onClick: () => {
930
1370
  setOpen(!open);
931
1371
  }