dsh-session-curator 0.1.2 → 0.2.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 CHANGED
@@ -37,21 +37,43 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
37
37
  // src/client/index.ts
38
38
  var index_exports = {};
39
39
  __export(index_exports, {
40
+ Dropdown: () => Dropdown,
40
41
  GroupHeader: () => GroupHeader,
41
42
  NS: () => NS,
42
43
  SessionRow: () => SessionRow,
44
+ TRASH_DAYS_MAX: () => TRASH_DAYS_MAX,
45
+ TRASH_DAYS_MIN: () => TRASH_DAYS_MIN,
46
+ TRASH_RETENTION_DAYS: () => TRASH_RETENTION_DAYS,
47
+ UsagePanel: () => UsagePanel,
43
48
  apply: () => apply,
49
+ cachedSnapshot: () => cachedSnapshot,
44
50
  collapseKey: () => collapseKey,
51
+ describeSession: () => describeSession,
52
+ dialogStore: () => dialogStore,
53
+ effectiveTrashDays: () => effectiveTrashDays,
54
+ filterRows: () => filterRows,
55
+ forgetDetails: () => forgetDetails,
56
+ forgetSnapshot: () => forgetSnapshot,
45
57
  groupKeyOf: () => groupKeyOf,
46
58
  groupRows: () => groupRows,
47
59
  inject: () => inject,
60
+ isFilterActive: () => isFilterActive,
48
61
  isGroupCollapsed: () => isGroupCollapsed,
62
+ matchesFilter: () => matchesFilter,
63
+ matchesSearch: () => matchesSearch,
64
+ parseCustomDays: () => parseCustomDays,
49
65
  sectionIdFor: () => sectionIdFor,
50
- toggledCollapse: () => toggledCollapse
66
+ showNotice: () => showNotice,
67
+ showTrashUndo: () => showTrashUndo,
68
+ sortRows: () => sortRows,
69
+ toastStore: () => toastStore,
70
+ toggledCollapse: () => toggledCollapse,
71
+ undoTrash: () => undoTrash,
72
+ workspaceSummaries: () => workspaceSummaries
51
73
  });
52
74
  module.exports = __toCommonJS(index_exports);
53
- var React6 = __toESM(require("react"), 1);
54
- var import_dsh_client_ui_primitives5 = require("@deepseek-ai/dsh-client-ui-primitives");
75
+ var React11 = __toESM(require("react"), 1);
76
+ var import_dsh_client_ui_primitives9 = require("@deepseek-ai/dsh-client-ui-primitives");
55
77
 
56
78
  // src/client/components/dialog-host.ts
57
79
  var React2 = __toESM(require("react"), 1);
@@ -174,6 +196,35 @@ function requestTrash(ids, title) {
174
196
  function requestPurge(ids, title) {
175
197
  dialogStore.set({ kind: "purge", ids, title });
176
198
  }
199
+ function requestPurgeOld(days, count, bytes) {
200
+ dialogStore.set({ kind: "purge-old", ids: [], title: "", days, count, bytes });
201
+ }
202
+
203
+ // src/client/format.ts
204
+ function formatBytes(bytes) {
205
+ if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes <= 0) return "0 B";
206
+ const units = ["B", "KB", "MB", "GB"];
207
+ let value = bytes;
208
+ let unit = 0;
209
+ while (value >= 1024 && unit < units.length - 1) {
210
+ value /= 1024;
211
+ unit += 1;
212
+ }
213
+ return `${value >= 10 || unit === 0 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
214
+ }
215
+ function formatTime(ms) {
216
+ if (typeof ms !== "number" || ms <= 0) return "";
217
+ try {
218
+ return new Date(ms).toLocaleString();
219
+ } catch {
220
+ return "";
221
+ }
222
+ }
223
+ function baseName(path) {
224
+ if (typeof path !== "string" || path === "") return "";
225
+ const parts = path.split(/[\\/]/).filter((part) => part !== "");
226
+ return parts.length > 0 ? parts[parts.length - 1] : path;
227
+ }
177
228
 
178
229
  // src/client/i18n.ts
179
230
  var NS = "session-curator";
@@ -193,6 +244,7 @@ var zh = {
193
244
  pin: "置顶",
194
245
  unpin: "取消置顶",
195
246
  restore: "恢复",
247
+ restoreUnavailable: "无法恢复(缺元数据)",
196
248
  purge: "永久删除",
197
249
  moveToTrash: "移入回收站",
198
250
  rowActions: "更多操作",
@@ -226,6 +278,36 @@ var zh = {
226
278
  clearShort: "取消",
227
279
  collapse: "折叠",
228
280
  expand: "展开",
281
+ // 跨页签的"整个工作区一起处理"(分组标题右侧的「⋯」)。
282
+ workspaceActions: "整个工作区",
283
+ workspaceMenu: "工作区操作",
284
+ archiveWorkspace: "整个工作区归档",
285
+ trashWorkspace: "整个工作区移入回收站",
286
+ workspaceRunningHint: "这个工作区里有会话正在运行,危险动作已禁用",
287
+ // 运行中徽标(宿主用官方的 workspace/session-activity 判定)。
288
+ running: "运行中",
289
+ runningHint: "会话正在运行,先停止它再操作",
290
+ // 导出 / 定位。
291
+ copyPath: "复制路径",
292
+ revealInFolder: "在资源管理器中显示",
293
+ pathCopied: "已复制路径:{path}",
294
+ pathCopyFailed: "复制失败,路径是:{path}",
295
+ revealFailed: "没能打开文件管理器({reason}),路径是:{path}",
296
+ // 磁盘占用视图。
297
+ titleUsage: "磁盘占用",
298
+ usageTitle: "磁盘占用",
299
+ usageTotal: "共 {n} 个会话 · {size}(磁盘上 {disk} 个)",
300
+ usageByWorkspace: "按工作区",
301
+ usageLargest: "最占地方的会话",
302
+ usageOldest: "最久没打开的会话",
303
+ usageLoading: "正在统计…",
304
+ usageArchived: "已归档",
305
+ usagePinned: "已置顶",
306
+ usageTrashed: "在回收站",
307
+ usageSuggestArchive: "建议归档",
308
+ usageArchive: "归档",
309
+ usageUnarchive: "取消归档",
310
+ usageNoData: "没有可统计的会话。",
229
311
  titleConfig: "配置",
230
312
  configTitle: "基础设置",
231
313
  configTab: "默认页签",
@@ -247,6 +329,33 @@ var zh = {
247
329
  orderHost: "原顺序",
248
330
  orderTitle: "标题",
249
331
  orderBytes: "体积",
332
+ orderCreatedAt: "创建时间",
333
+ orderDeletedAt: "删除时间",
334
+ // 搜索与过滤(只影响显示;批量操作与计数仍按整个页签算)。
335
+ searchLabel: "搜索会话",
336
+ searchPlaceholder: "搜索标题或目录…",
337
+ searchClear: "清除搜索",
338
+ timeFilterLabel: "时间",
339
+ timeAll: "全部",
340
+ timeRecent: "最近 7 天",
341
+ filtered: "已过滤出 {n} 个会话(共 {total} 个)",
342
+ emptyFiltered: "没有符合搜索条件的会话。",
343
+ // 回收站保留策略(永久删除:永远先预览、再确认,绝不自动后台清理)。
344
+ configTrashDays: "回收站保留天数",
345
+ configTrashDaysHint: "「清理 N 天前」按这个天数算;不会自动执行,每次都要先看预览再确认",
346
+ retentionDays: "{n} 天",
347
+ retentionCustom: "自定义",
348
+ customDaysPlaceholder: "天数",
349
+ customDaysLabel: "自定义保留天数",
350
+ customDaysHint: "填 {min}–{max} 之间的整数;留空就用上面的档位",
351
+ customDaysInvalid: "天数要在 {min}–{max} 之间",
352
+ trashCleanup: "删除 {n} 天前的会话",
353
+ trashPreview: "将删除 {n} 个会话 · {size}",
354
+ trashPreviewNone: "没有超过 {days} 天的会话",
355
+ trashPreviewLoading: "正在统计…",
356
+ confirmPurgeOldTitle: "清理旧会话",
357
+ confirmPurgeOldBody: "将永久删除 {n} 个在 {days} 天前移入回收站的会话({size}),无法恢复。",
358
+ ackPurgeOld: "我明白这无法撤销",
250
359
  configAutoUpdate: "自动检查更新",
251
360
  configAutoUpdateHint: "打开这个页面时问一次 npm registry(10 分钟内有缓存)",
252
361
  configReset: "恢复默认",
@@ -263,7 +372,19 @@ var zh = {
263
372
  updateStepsTitle: "升级步骤",
264
373
  updateStepOne: "在终端里升级包(按你自己的 profile 名替换 desktop):",
265
374
  updateStepTwo: "重启 DSH 让宿主半区生效;只改了客户端半区的话刷新页面就够。",
266
- updateOpen: "打开 npm 页面"
375
+ updateOpen: "打开 npm 页面",
376
+ // 会话行 hover 卡片(`sidebar.session.row.hover`)。
377
+ hoverStats: "{size} · {n} 个文件",
378
+ hoverCwd: "原目录",
379
+ hoverArchived: "已归档",
380
+ hoverPinned: "已置顶",
381
+ hoverTrashed: "在回收站",
382
+ hoverDeleted: "删除于",
383
+ // 删除成功后的撤销提示(`Toast`)。
384
+ toastTrashed: "「{title}」已移入回收站",
385
+ toastTrashedMany: "{n} 个会话已移入回收站",
386
+ toastUndo: "撤销",
387
+ toastUndoFailed: "撤销失败:{reason}"
267
388
  };
268
389
  var en = {
269
390
  sectionLabel: "Session manager",
@@ -281,6 +402,7 @@ var en = {
281
402
  pin: "Pin",
282
403
  unpin: "Unpin",
283
404
  restore: "Restore",
405
+ restoreUnavailable: "Cannot restore (no metadata)",
284
406
  purge: "Delete permanently",
285
407
  moveToTrash: "Move to trash",
286
408
  rowActions: "More actions",
@@ -314,6 +436,36 @@ var en = {
314
436
  clearShort: "Clear",
315
437
  collapse: "Collapse",
316
438
  expand: "Expand",
439
+ // Cross-tab "handle the whole workspace" (the ⋯ on a folder header).
440
+ workspaceActions: "Whole workspace",
441
+ workspaceMenu: "Workspace actions",
442
+ archiveWorkspace: "Archive this whole workspace",
443
+ trashWorkspace: "Move this whole workspace to the trash",
444
+ workspaceRunningHint: "A session in this workspace is running, so the destructive actions are off",
445
+ // Running badge (the host asks the official workspace/session-activity hook).
446
+ running: "Running",
447
+ runningHint: "This session is running; stop it first",
448
+ // Export / locate.
449
+ copyPath: "Copy path",
450
+ revealInFolder: "Show in file explorer",
451
+ pathCopied: "Path copied: {path}",
452
+ pathCopyFailed: "Copy failed — the path is: {path}",
453
+ revealFailed: "Could not open the file explorer ({reason}); the path is: {path}",
454
+ // Disk usage view.
455
+ titleUsage: "Disk usage",
456
+ usageTitle: "Disk usage",
457
+ usageTotal: "{n} sessions · {size} ({disk} on disk)",
458
+ usageByWorkspace: "By workspace",
459
+ usageLargest: "Largest sessions",
460
+ usageOldest: "Least recently opened",
461
+ usageLoading: "Measuring…",
462
+ usageArchived: "Archived",
463
+ usagePinned: "Pinned",
464
+ usageTrashed: "In trash",
465
+ usageSuggestArchive: "Archive suggested",
466
+ usageArchive: "Archive",
467
+ usageUnarchive: "Unarchive",
468
+ usageNoData: "No sessions to measure.",
317
469
  titleConfig: "Settings",
318
470
  configTitle: "Basics",
319
471
  configTab: "Default tab",
@@ -335,6 +487,33 @@ var en = {
335
487
  orderHost: "Host",
336
488
  orderTitle: "Title",
337
489
  orderBytes: "Size",
490
+ orderCreatedAt: "Created",
491
+ orderDeletedAt: "Deleted",
492
+ // Search and filtering (display only; batch actions and counts still cover the whole tab).
493
+ searchLabel: "Search sessions",
494
+ searchPlaceholder: "Search title or folder…",
495
+ searchClear: "Clear search",
496
+ timeFilterLabel: "Time",
497
+ timeAll: "All",
498
+ timeRecent: "Last 7 days",
499
+ filtered: "{n} of {total} sessions match",
500
+ emptyFiltered: "No session matches the search.",
501
+ // Trash retention (permanent deletion: always preview, then confirm; never automatic).
502
+ configTrashDays: "Trash retention",
503
+ configTrashDaysHint: "How “clean up N days ago” counts the days; it never runs on its own — every cleanup needs a preview and a confirmation",
504
+ retentionDays: "{n} days",
505
+ retentionCustom: "Custom",
506
+ customDaysPlaceholder: "Days",
507
+ customDaysLabel: "Custom retention days",
508
+ customDaysHint: "An integer between {min} and {max}; leave it empty to use the preset above",
509
+ customDaysInvalid: "Days must be between {min} and {max}",
510
+ trashCleanup: "Delete sessions older than {n} days",
511
+ trashPreview: "{n} sessions · {size} will be deleted",
512
+ trashPreviewNone: "Nothing is older than {days} days",
513
+ trashPreviewLoading: "Counting…",
514
+ confirmPurgeOldTitle: "Clean up old sessions",
515
+ confirmPurgeOldBody: "{n} sessions moved to the trash more than {days} days ago ({size}) are deleted for good and cannot be recovered.",
516
+ ackPurgeOld: "I understand this cannot be undone",
338
517
  configAutoUpdate: "Check for updates automatically",
339
518
  configAutoUpdateHint: "Asks the npm registry when this page opens (cached for 10 minutes)",
340
519
  configReset: "Reset to defaults",
@@ -351,7 +530,19 @@ var en = {
351
530
  updateStepsTitle: "How to upgrade",
352
531
  updateStepOne: "Upgrade the package in a terminal (replace desktop with your profile name):",
353
532
  updateStepTwo: "Restart DSH so the host half reloads; a client-half change only needs a page refresh.",
354
- updateOpen: "Open the npm page"
533
+ updateOpen: "Open the npm page",
534
+ // Session-row hover card (`sidebar.session.row.hover`).
535
+ hoverStats: "{size} · {n} files",
536
+ hoverCwd: "Folder",
537
+ hoverArchived: "Archived",
538
+ hoverPinned: "Pinned",
539
+ hoverTrashed: "In trash",
540
+ hoverDeleted: "Deleted",
541
+ // Undo notice after a successful delete (`Toast`).
542
+ toastTrashed: "“{title}” moved to the trash",
543
+ toastTrashedMany: "{n} sessions moved to the trash",
544
+ toastUndo: "Undo",
545
+ toastUndoFailed: "Undo failed: {reason}"
355
546
  };
356
547
  var dictionaries = { zh, en };
357
548
  function text(t, key, params) {
@@ -367,7 +558,33 @@ var TAB_LABEL = {
367
558
  trash: "titleTrash"
368
559
  };
369
560
 
561
+ // src/client/snapshot.ts
562
+ var last = null;
563
+ function cachedSnapshot() {
564
+ return last;
565
+ }
566
+ function rememberSnapshot(value) {
567
+ last = value;
568
+ }
569
+ function forgetSnapshot() {
570
+ last = null;
571
+ }
572
+
370
573
  // src/client/styles.ts
574
+ var ROW_ICON_BUTTON = {
575
+ display: "inline-flex",
576
+ alignItems: "center",
577
+ justifyContent: "center",
578
+ width: "16px",
579
+ height: "16px",
580
+ padding: 0,
581
+ margin: 0,
582
+ border: "none",
583
+ background: "transparent",
584
+ borderRadius: "var(--dsw-radius-xs)",
585
+ cursor: "pointer",
586
+ flex: "none"
587
+ };
371
588
  var S = {
372
589
  column: { display: "flex", flexDirection: "column", gap: "10px", padding: "4px 0", minWidth: 0 },
373
590
  rowLine: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" },
@@ -386,12 +603,20 @@ var S = {
386
603
  borderTop: "1px solid var(--dsw-alias-border-l1)"
387
604
  },
388
605
  itemMain: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0, flex: "1 1 auto" },
606
+ /**
607
+ * 标题那一行:标题 + 状态胶囊(「运行中」)。
608
+ *
609
+ * 胶囊 `flex: 0 0 auto`、标题 `minWidth: 0` + 省略号 —— 长标题只挤自己那一段,
610
+ * 不会把胶囊压成两行(与分组标题那次"长路径挤坏按钮"同一条教训,见 groupAction)。
611
+ */
612
+ itemTitleRow: { display: "flex", alignItems: "center", gap: "6px", minWidth: 0 },
389
613
  itemTitle: {
390
614
  color: "var(--dsw-alias-label-primary)",
391
615
  fontSize: "13px",
392
616
  overflow: "hidden",
393
617
  textOverflow: "ellipsis",
394
- whiteSpace: "nowrap"
618
+ whiteSpace: "nowrap",
619
+ minWidth: 0
395
620
  },
396
621
  itemMeta: {
397
622
  color: "var(--dsw-alias-label-secondary)",
@@ -400,6 +625,35 @@ var S = {
400
625
  textOverflow: "ellipsis",
401
626
  whiteSpace: "nowrap"
402
627
  },
628
+ /** 状态胶囊:与 hover 卡片那几个同款(带 alpha 的叠加色,深浅主题都有效)。 */
629
+ badge: {
630
+ color: "var(--dsw-alias-label-secondary)",
631
+ fontSize: "10px",
632
+ lineHeight: "16px",
633
+ padding: "0 6px",
634
+ borderRadius: "8px",
635
+ background: "var(--dsw-alias-interactive-bg-active)",
636
+ flex: "0 0 auto"
637
+ },
638
+ badgeDanger: {
639
+ color: "var(--dsw-alias-state-error-primary)",
640
+ fontSize: "10px",
641
+ lineHeight: "16px",
642
+ padding: "0 6px",
643
+ borderRadius: "8px",
644
+ background: "var(--dsw-alias-interactive-bg-active)",
645
+ flex: "0 0 auto"
646
+ },
647
+ /** 「运行中」:同款胶囊,但用业务蓝表示"它是活的",不是危险。 */
648
+ badgeRunning: {
649
+ color: "var(--dsw-alias-state-business-primary)",
650
+ fontSize: "10px",
651
+ lineHeight: "16px",
652
+ padding: "0 6px",
653
+ borderRadius: "8px",
654
+ background: "var(--dsw-alias-interactive-bg-active)",
655
+ flex: "0 0 auto"
656
+ },
403
657
  itemActions: { display: "flex", alignItems: "center", gap: "6px", flex: "0 0 auto" },
404
658
  /**
405
659
  * 「⋯」触发器:图标按钮,弱化成次要前景色(跟侧边栏行的图标按钮一致)。
@@ -472,6 +726,48 @@ var S = {
472
726
  empty: { color: "var(--dsw-alias-label-secondary)", fontSize: "12px", padding: "14px 10px", textAlign: "center" },
473
727
  error: { color: "var(--dsw-alias-state-error-primary)", fontSize: "12px" },
474
728
  footer: { color: "var(--dsw-alias-label-secondary)", fontSize: "11px", display: "flex", gap: "10px", flexWrap: "wrap" },
729
+ /**
730
+ * 数据页签上的过滤工具条:搜索框在左、时间档在右。
731
+ *
732
+ * `flexWrap` 是必需的:设置页本身可能很窄(侧边栏展开时尤其),搜索框的
733
+ * `minWidth` 兜住它不被压成一条线,挤不下时时间档换行,而不是把输入框挤没。
734
+ */
735
+ filterBar: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" },
736
+ /** 搜索框自己撑开剩余宽度;给个下限,窄屏下也不至于只剩图标。 */
737
+ searchInput: { flex: "1 1 auto", minWidth: "160px" },
738
+ /** 把时间档推到右边(搜索框与它之间那段空白)。 */
739
+ filterSpacer: { flex: "1 1 auto" },
740
+ // ── 磁盘占用视图(配置页里的一块) ─────────────────────────────────────
741
+ usageBody: { display: "flex", flexDirection: "column", padding: "0 12px 12px", gap: "2px" },
742
+ usageHeading: {
743
+ color: "var(--dsw-alias-label-tertiary)",
744
+ fontSize: "11px",
745
+ marginTop: "8px",
746
+ marginBottom: "2px"
747
+ },
748
+ /** 一条工作区横条:轨道 + 名字 + 数字。名字可以很长,所以它自己省略。 */
749
+ usageBarRow: { display: "flex", alignItems: "center", gap: "8px", minWidth: 0 },
750
+ usageBarTrack: {
751
+ width: "70px",
752
+ height: "6px",
753
+ borderRadius: "3px",
754
+ background: "var(--dsw-alias-interactive-bg-active)",
755
+ flex: "0 0 auto",
756
+ overflow: "hidden"
757
+ },
758
+ usageBarFill: { display: "block", height: "100%", background: "var(--dsw-alias-state-business-primary)" },
759
+ usageBarName: {
760
+ color: "var(--dsw-alias-label-primary)",
761
+ fontSize: "12px",
762
+ overflow: "hidden",
763
+ textOverflow: "ellipsis",
764
+ whiteSpace: "nowrap",
765
+ minWidth: 0,
766
+ flex: "1 1 auto"
767
+ },
768
+ usageBarMeta: { color: "var(--dsw-alias-label-secondary)", fontSize: "11px", flex: "0 0 auto" },
769
+ /** 榜单里的一行:与列表行同一套几何,只是没有勾选框。 */
770
+ usageRow: { display: "flex", alignItems: "center", gap: "10px", padding: "6px 0", minWidth: 0 },
475
771
  mono: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" },
476
772
  ack: { display: "flex", alignItems: "center", gap: "8px", fontSize: "12px", color: "var(--dsw-alias-label-primary)" },
477
773
  /** 顶部条:页签在左,动作靠 `marginInlineStart: auto` 推到右边。 */
@@ -490,6 +786,14 @@ var S = {
490
786
  configLabel: { color: "var(--dsw-alias-label-primary)", fontSize: "13px" },
491
787
  configHint: { color: "var(--dsw-alias-label-tertiary)", fontSize: "11px" },
492
788
  configControl: { display: "flex", alignItems: "center", gap: "8px", flex: "0 0 auto" },
789
+ /** 下拉锚点:文字 + 双向箭头图标,别被挤窄(长标签在按钮里折行是这个仓库踩过的坑)。 */
790
+ dropdownAnchor: { flex: "0 0 auto" },
791
+ dropdownButton: { flex: "0 0 auto", whiteSpace: "nowrap" },
792
+ /** 保留策略那一格:档位下拉 + 自定义天数输入框。 */
793
+ retentionDays: { display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap", justifyContent: "flex-end" },
794
+ customDaysInput: { width: "88px", flex: "0 0 auto" },
795
+ /** 天数非法时只把边框标红,不动尺寸(否则整行会跳一下)。 */
796
+ customDaysInvalid: { borderColor: "var(--dsw-alias-state-error-primary)" },
493
797
  configFoot: {
494
798
  display: "flex",
495
799
  alignItems: "center",
@@ -525,9 +829,99 @@ var S = {
525
829
  overflowX: "auto",
526
830
  whiteSpace: "pre"
527
831
  },
528
- link: { color: "var(--dsw-alias-label-primary)", fontSize: "11px" }
832
+ link: { color: "var(--dsw-alias-label-primary)", fontSize: "11px" },
833
+ /**
834
+ * 会话行 hover 卡片里我们那一节。卡片自己的 `hoverContent` 已经是纵向 flex 且有间距,
835
+ * 这里只补一点点上边距,不加分隔线 —— 上面那行是官方画的相对时间。
836
+ */
837
+ hoverBlock: { display: "flex", flexDirection: "column", gap: "3px", marginTop: "2px", minWidth: 0 },
838
+ hoverLine: { display: "flex", alignItems: "center", gap: "6px", minWidth: 0 },
839
+ hoverLabel: { color: "var(--dsw-alias-label-tertiary)", fontSize: "11px", flex: "0 0 auto" },
840
+ hoverMeta: { color: "var(--dsw-alias-label-secondary)", fontSize: "11px", flex: "0 0 auto" },
841
+ /** 原目录可能很长:让它自己省略,别把卡片撑宽。 */
842
+ hoverPath: {
843
+ color: "var(--dsw-alias-label-secondary)",
844
+ fontSize: "11px",
845
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
846
+ overflow: "hidden",
847
+ textOverflow: "ellipsis",
848
+ whiteSpace: "nowrap",
849
+ minWidth: 0,
850
+ flex: "0 1 auto"
851
+ },
852
+ hoverBadges: { display: "flex", alignItems: "center", gap: "4px", flexWrap: "wrap" },
853
+ /** 状态胶囊:底色用带 alpha 的 `interactive-bg-active`(浅色主题里 layer 系列是同一个颜色)。 */
854
+ hoverBadge: {
855
+ color: "var(--dsw-alias-label-secondary)",
856
+ fontSize: "10px",
857
+ lineHeight: "16px",
858
+ padding: "0 6px",
859
+ borderRadius: "8px",
860
+ background: "var(--dsw-alias-interactive-bg-active)",
861
+ flex: "0 0 auto"
862
+ },
863
+ hoverBadgeDanger: {
864
+ color: "var(--dsw-alias-state-error-primary)",
865
+ fontSize: "10px",
866
+ lineHeight: "16px",
867
+ padding: "0 6px",
868
+ borderRadius: "8px",
869
+ background: "var(--dsw-alias-interactive-bg-active)",
870
+ flex: "0 0 auto"
871
+ },
872
+ /**
873
+ * 侧边栏会话行 hover 时那个「移入回收站」图标按钮(见
874
+ * `components/trash-row-button.ts`):16×16 的朴素按钮,**两种状态都没有底色**,
875
+ * 只有前景色变深一档。
876
+ *
877
+ * `color-mix` 是 DSH 自己的产物里就在用的写法(例如 ui-deliverables 的
878
+ * `color-mix(in srgb, var(--dsw-alias-interactive-bg-hover) 50%, transparent)`)。
879
+ * 为什么不换 `state-error-secondary`:那个 token 在浅色主题里是 red-400,比 primary
880
+ * (red-600)**更浅**,方向正好相反。
881
+ */
882
+ rowIconButton: { ...ROW_ICON_BUTTON, color: "var(--dsw-alias-state-error-primary)" },
883
+ rowIconButtonHover: { ...ROW_ICON_BUTTON, color: "color-mix(in srgb, var(--dsw-alias-state-error-primary) 78%, #000)" }
529
884
  };
530
885
 
886
+ // src/client/toast.ts
887
+ var toastStore = createStore(null);
888
+ var sequence = 0;
889
+ function showTrashUndo(ids, title) {
890
+ if (ids.length === 0) return;
891
+ sequence += 1;
892
+ toastStore.set({ seq: sequence, ids: [...ids], title, error: null });
893
+ }
894
+ function showNotice(message) {
895
+ if (message === "") return;
896
+ sequence += 1;
897
+ toastStore.set({ seq: sequence, ids: [], title: message, error: null });
898
+ }
899
+ function dismissToast() {
900
+ toastStore.set(null);
901
+ }
902
+ function failToast(reason) {
903
+ const current2 = toastStore.get();
904
+ if (current2 === null) return;
905
+ toastStore.set({ ...current2, error: reason });
906
+ }
907
+ async function undoTrash() {
908
+ const current2 = toastStore.get();
909
+ if (current2 === null || current2.error !== null) return;
910
+ try {
911
+ const value = await call("restore", { ids: current2.ids });
912
+ const failure = firstFailure(value.results);
913
+ if (failure !== null) {
914
+ failToast(failure);
915
+ return;
916
+ }
917
+ mutationStore.set(mutationStore.get() + 1);
918
+ rememberSnapshot(value);
919
+ dismissToast();
920
+ } catch (thrown) {
921
+ failToast(reportError(thrown));
922
+ }
923
+ }
924
+
531
925
  // src/client/components/dialog-host.ts
532
926
  function DialogHost(props) {
533
927
  const t = props.t;
@@ -535,16 +929,18 @@ function DialogHost(props) {
535
929
  const [busy, setBusy] = React2.useState(false);
536
930
  const [ack, setAck] = React2.useState(false);
537
931
  const [error, setError] = React2.useState(null);
538
- const requestKey = request === null ? "" : `${request.kind}:${request.ids.join(",")}:${request.title}`;
932
+ const requestKey = request === null ? "" : `${request.kind}:${request.ids.join(",")}:${request.title}:${request.days ?? ""}`;
539
933
  React2.useEffect(() => {
540
934
  setAck(false);
541
935
  setError(null);
542
936
  }, [requestKey]);
543
937
  if (request === null) return null;
544
- const purge = request.kind === "purge";
938
+ const purgeOld = request.kind === "purge-old";
939
+ const purge = request.kind === "purge" || purgeOld;
545
940
  const many = request.ids.length > 1;
546
- const title = purge ? t("confirmPurgeTitle") : t("confirmTrashTitle");
547
- const body = purge ? many ? text(t, "confirmPurgeManyBody", { n: request.ids.length }) : text(t, "confirmPurgeBody", { title: request.title }) : text(t, "confirmTrashBody", { title: request.title });
941
+ const days = request.days ?? 0;
942
+ const title = purgeOld ? t("confirmPurgeOldTitle") : purge ? t("confirmPurgeTitle") : t("confirmTrashTitle");
943
+ const body = purgeOld ? text(t, "confirmPurgeOldBody", { n: request.count ?? 0, days, size: formatBytes(request.bytes ?? 0) }) : purge ? many ? text(t, "confirmPurgeManyBody", { n: request.ids.length }) : text(t, "confirmPurgeBody", { title: request.title }) : text(t, "confirmTrashBody", { title: request.title });
548
944
  const close = () => {
549
945
  if (busy) return;
550
946
  dialogStore.set(null);
@@ -552,11 +948,14 @@ function DialogHost(props) {
552
948
  const confirm = async () => {
553
949
  setBusy(true);
554
950
  try {
555
- const value = await call(purge ? "purge" : "trash", { ids: request.ids });
951
+ const endpoint = purgeOld ? "purge-older-than" : purge ? "purge" : "trash";
952
+ const value = await call(endpoint, purgeOld ? { days } : { ids: request.ids });
556
953
  const failure = firstFailure(value.results);
557
954
  if (failure === null) {
558
955
  dialogStore.set(null);
956
+ rememberSnapshot(value);
559
957
  mutationStore.set(mutationStore.get() + 1);
958
+ if (!purge) showTrashUndo(request.ids, request.title);
560
959
  } else {
561
960
  setError(failure);
562
961
  }
@@ -578,12 +977,16 @@ function DialogHost(props) {
578
977
  "div",
579
978
  { style: S.rowLine },
580
979
  React2.createElement(import_dsh_client_ui_primitives.Button, { size: "md", variant: "ghost", disabled: busy, onClick: close }, t("cancel")),
581
- React2.createElement(import_dsh_client_ui_primitives.Button, {
582
- size: "md",
583
- variant: "primary",
584
- disabled: busy || purge && !ack,
585
- onClick: () => void confirm()
586
- }, purge ? t("confirmPurgeAction") : t("confirmTrashAction"))
980
+ React2.createElement(
981
+ import_dsh_client_ui_primitives.Button,
982
+ {
983
+ size: "md",
984
+ variant: "primary",
985
+ disabled: busy || purge && !ack,
986
+ onClick: () => void confirm()
987
+ },
988
+ purge ? t("confirmPurgeAction") : t("confirmTrashAction")
989
+ )
587
990
  )
588
991
  },
589
992
  purge ? React2.createElement(
@@ -598,44 +1001,172 @@ function DialogHost(props) {
598
1001
  disabled: busy,
599
1002
  onChange: (event) => setAck(Boolean(event.target.checked))
600
1003
  }),
601
- t("ackPurge")
1004
+ purgeOld ? t("ackPurgeOld") : t("ackPurge")
602
1005
  ),
603
1006
  error ? React2.createElement("div", { style: S.error }, error) : null
604
1007
  ) : error ? React2.createElement("div", { style: S.error }, error) : null
605
1008
  );
606
1009
  }
607
1010
 
608
- // src/client/components/settings-panel.ts
609
- var React5 = __toESM(require("react"), 1);
610
- var import_dsh_client_ui_primitives4 = require("@deepseek-ai/dsh-client-ui-primitives");
1011
+ // src/client/components/session-hover.ts
1012
+ var React3 = __toESM(require("react"), 1);
611
1013
 
612
- // src/client/format.ts
613
- function formatBytes(bytes) {
614
- if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes <= 0) return "0 B";
615
- const units = ["B", "KB", "MB", "GB"];
616
- let value = bytes;
617
- let unit = 0;
618
- while (value >= 1024 && unit < units.length - 1) {
619
- value /= 1024;
620
- unit += 1;
1014
+ // src/client/detail.ts
1015
+ var TTL_MS = 3e4;
1016
+ var cache = /* @__PURE__ */ new Map();
1017
+ var inflight = /* @__PURE__ */ new Map();
1018
+ var watching = false;
1019
+ function forgetDetails() {
1020
+ cache.clear();
1021
+ }
1022
+ function watchMutations() {
1023
+ if (watching) return;
1024
+ watching = true;
1025
+ mutationStore.subscribe(forgetDetails);
1026
+ }
1027
+ async function describeSession(sessionId) {
1028
+ watchMutations();
1029
+ const cached = cache.get(sessionId);
1030
+ if (cached !== void 0 && Date.now() - cached.at < TTL_MS) return cached.value;
1031
+ const running = inflight.get(sessionId);
1032
+ if (running !== void 0) return running;
1033
+ const request = call("describe", { ids: [sessionId] }).then((value) => {
1034
+ cache.set(sessionId, { at: Date.now(), value });
1035
+ return value;
1036
+ }).finally(() => {
1037
+ inflight.delete(sessionId);
1038
+ });
1039
+ inflight.set(sessionId, request);
1040
+ return request;
1041
+ }
1042
+
1043
+ // src/client/components/session-hover.ts
1044
+ function SessionHover(props) {
1045
+ const t = props.t;
1046
+ const [detail, setDetail] = React3.useState(null);
1047
+ React3.useEffect(() => {
1048
+ let live = true;
1049
+ describeSession(props.sessionId).then(
1050
+ (value) => {
1051
+ if (live) setDetail(value);
1052
+ },
1053
+ () => {
1054
+ }
1055
+ );
1056
+ return () => {
1057
+ live = false;
1058
+ };
1059
+ }, [props.sessionId]);
1060
+ if (detail === null) return null;
1061
+ const badges = [];
1062
+ if (detail.trashed) badges.push({ key: "trashed", label: t("hoverTrashed"), danger: true });
1063
+ if (detail.archived) badges.push({ key: "archived", label: t("hoverArchived"), danger: false });
1064
+ if (detail.pinned) badges.push({ key: "pinned", label: t("hoverPinned"), danger: false });
1065
+ const lines = [
1066
+ React3.createElement(
1067
+ "div",
1068
+ { key: "stats", style: S.hoverLine },
1069
+ React3.createElement("span", { style: S.hoverMeta }, text(t, "hoverStats", { size: formatBytes(detail.bytes), n: detail.files }))
1070
+ )
1071
+ ];
1072
+ if (detail.cwd) {
1073
+ lines.push(
1074
+ React3.createElement(
1075
+ "div",
1076
+ { key: "cwd", style: S.hoverLine },
1077
+ React3.createElement("span", { style: S.hoverLabel }, t("hoverCwd")),
1078
+ React3.createElement("span", { style: S.hoverPath, title: detail.cwd }, detail.cwd)
1079
+ )
1080
+ );
621
1081
  }
622
- return `${value >= 10 || unit === 0 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
1082
+ if (detail.trashed && detail.deletedAt) {
1083
+ lines.push(
1084
+ React3.createElement(
1085
+ "div",
1086
+ { key: "deletedAt", style: S.hoverLine },
1087
+ React3.createElement("span", { style: S.hoverLabel }, t("hoverDeleted")),
1088
+ React3.createElement("span", { style: S.hoverMeta }, formatTime(detail.deletedAt))
1089
+ )
1090
+ );
1091
+ }
1092
+ if (badges.length > 0) {
1093
+ lines.push(
1094
+ React3.createElement(
1095
+ "div",
1096
+ { key: "badges", style: S.hoverBadges },
1097
+ badges.map(
1098
+ (badge) => React3.createElement(
1099
+ "span",
1100
+ {
1101
+ key: badge.key,
1102
+ style: badge.danger ? S.hoverBadgeDanger : S.hoverBadge
1103
+ },
1104
+ badge.label
1105
+ )
1106
+ )
1107
+ )
1108
+ );
1109
+ }
1110
+ return React3.createElement("div", { style: S.hoverBlock }, lines);
623
1111
  }
624
- function formatTime(ms) {
625
- if (typeof ms !== "number" || ms <= 0) return "";
1112
+
1113
+ // src/client/components/settings-panel.ts
1114
+ var React8 = __toESM(require("react"), 1);
1115
+ var import_dsh_client_ui_primitives6 = require("@deepseek-ai/dsh-client-ui-primitives");
1116
+
1117
+ // src/client/export.ts
1118
+ function openSessionPath(sessionId, reveal) {
1119
+ return call("open", reveal === true ? { ids: [sessionId], target: "reveal" } : { ids: [sessionId], target: "path" });
1120
+ }
1121
+ async function copyText(value) {
1122
+ const clipboard = globalThis.navigator?.clipboard;
1123
+ if (clipboard !== void 0 && typeof clipboard.writeText === "function") {
1124
+ try {
1125
+ await clipboard.writeText(value);
1126
+ return true;
1127
+ } catch {
1128
+ }
1129
+ }
626
1130
  try {
627
- return new Date(ms).toLocaleString();
1131
+ const document = globalThis.document;
1132
+ if (document === void 0) return false;
1133
+ const area = document.createElement("textarea");
1134
+ area.value = value;
1135
+ area.style.position = "fixed";
1136
+ area.style.top = "-1000px";
1137
+ area.style.opacity = "0";
1138
+ document.body.appendChild(area);
1139
+ area.select();
1140
+ const copied = document.execCommand("copy");
1141
+ document.body.removeChild(area);
1142
+ return copied;
628
1143
  } catch {
629
- return "";
1144
+ return false;
630
1145
  }
631
1146
  }
632
- function baseName(path) {
633
- if (typeof path !== "string" || path === "") return "";
634
- const parts = path.split(/[\\/]/).filter((part) => part !== "");
635
- return parts.length > 0 ? parts[parts.length - 1] : path;
636
- }
637
1147
 
638
1148
  // src/client/groups.ts
1149
+ function workspaceSummaries(archived, pinned, trashed) {
1150
+ const summaries = /* @__PURE__ */ new Map();
1151
+ const bucketOf = (key) => {
1152
+ let summary = summaries.get(key);
1153
+ if (summary === void 0) {
1154
+ summary = { key, archivableIds: [], trashableIds: [], activeIds: [] };
1155
+ summaries.set(key, summary);
1156
+ }
1157
+ return summary;
1158
+ };
1159
+ for (const rows of [archived, pinned]) {
1160
+ for (const row of rows) {
1161
+ const summary = bucketOf(groupKeyOf(row));
1162
+ summary.archivableIds.push(row.sessionId);
1163
+ summary.trashableIds.push(row.sessionId);
1164
+ if (row.active === true) summary.activeIds.push(row.sessionId);
1165
+ }
1166
+ }
1167
+ for (const row of trashed) bucketOf(groupKeyOf(row));
1168
+ return summaries;
1169
+ }
639
1170
  function groupKeyOf(row) {
640
1171
  if (typeof row.cwd === "string" && row.cwd.trim() !== "") {
641
1172
  return `cwd:${row.cwd.trim().replace(/[\\/]+$/, "").replace(/\\/g, "/").toLowerCase()}`;
@@ -667,14 +1198,55 @@ function groupRows(rows, t) {
667
1198
  return list;
668
1199
  }
669
1200
  function sortRows(rows, order) {
1201
+ const descending = (pick) => (left, right) => {
1202
+ const a = pick(left);
1203
+ const b = pick(right);
1204
+ const hasA = typeof a === "number" && Number.isFinite(a) && a > 0;
1205
+ const hasB = typeof b === "number" && Number.isFinite(b) && b > 0;
1206
+ if (!hasA && !hasB) return 0;
1207
+ if (!hasA) return 1;
1208
+ if (!hasB) return -1;
1209
+ return b - a;
1210
+ };
670
1211
  if (order === "title") {
671
1212
  return [...rows].sort((left, right) => String(left.title ?? "").localeCompare(String(right.title ?? "")));
672
1213
  }
673
1214
  if (order === "bytes") {
674
1215
  return [...rows].sort((left, right) => (right.bytes ?? 0) - (left.bytes ?? 0));
675
1216
  }
1217
+ if (order === "createdAt") return [...rows].sort(descending((row) => row.createdAt));
1218
+ if (order === "deletedAt") return [...rows].sort(descending((row) => row.deletedAt));
676
1219
  return rows;
677
1220
  }
1221
+ var RECENT_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
1222
+ function isTrashRow(row) {
1223
+ return typeof row.deletedAt === "number";
1224
+ }
1225
+ function matchesSearch(row, search) {
1226
+ const needle = search.trim().toLowerCase();
1227
+ if (needle === "") return true;
1228
+ const haystacks = [row.title, row.cwd, row.workspaceTitle];
1229
+ return haystacks.some((value) => typeof value === "string" && value.toLowerCase().includes(needle));
1230
+ }
1231
+ function timeStampOf(row) {
1232
+ if (isTrashRow(row)) return typeof row.deletedAt === "number" && row.deletedAt > 0 ? row.deletedAt : null;
1233
+ if (typeof row.createdAt === "number" && row.createdAt > 0) return row.createdAt;
1234
+ return null;
1235
+ }
1236
+ function matchesFilter(row, filter) {
1237
+ if (!matchesSearch(row, filter.search)) return false;
1238
+ if (filter.timeFilter !== "recent") return true;
1239
+ const stamp = timeStampOf(row);
1240
+ if (stamp === null) return false;
1241
+ return (filter.now ?? Date.now()) - stamp <= RECENT_WINDOW_MS;
1242
+ }
1243
+ function filterRows(rows, filter) {
1244
+ if (filter.search.trim() === "" && filter.timeFilter === "all") return rows;
1245
+ return rows.filter((row) => matchesFilter(row, filter));
1246
+ }
1247
+ function isFilterActive(filter) {
1248
+ return filter.search.trim() !== "" || filter.timeFilter !== "all";
1249
+ }
678
1250
  function collapseKey(tab, groupKey) {
679
1251
  return `${tab}:${groupKey}`;
680
1252
  }
@@ -691,6 +1263,21 @@ function toggledCollapse(key, overrides, fallback) {
691
1263
  }
692
1264
 
693
1265
  // src/client/settings.ts
1266
+ var TRASH_RETENTION_DAYS = [7, 30, 90];
1267
+ var DEFAULT_TRASH_DAYS = 30;
1268
+ var TRASH_DAYS_MIN = 1;
1269
+ var TRASH_DAYS_MAX = 3650;
1270
+ function parseCustomDays(raw) {
1271
+ const trimmed = raw.trim();
1272
+ if (trimmed === "") return null;
1273
+ if (!/^\d+$/.test(trimmed)) return null;
1274
+ const value = Number(trimmed);
1275
+ if (!Number.isSafeInteger(value) || value < TRASH_DAYS_MIN || value > TRASH_DAYS_MAX) return null;
1276
+ return value;
1277
+ }
1278
+ function effectiveTrashDays(settings) {
1279
+ return parseCustomDays(settings.trashDaysCustom) ?? settings.trashDays;
1280
+ }
694
1281
  var DEFAULT_SETTINGS = {
695
1282
  tab: "archived",
696
1283
  grouping: true,
@@ -698,8 +1285,13 @@ var DEFAULT_SETTINGS = {
698
1285
  groupDefault: "expanded",
699
1286
  showMeta: true,
700
1287
  order: "host",
701
- autoUpdate: true
1288
+ autoUpdate: true,
1289
+ search: "",
1290
+ timeFilter: "all",
1291
+ trashDays: DEFAULT_TRASH_DAYS,
1292
+ trashDaysCustom: ""
702
1293
  };
1294
+ var SEARCH_MAX_LENGTH = 200;
703
1295
  var SETTINGS_KEY = "dsh-session-curator.settings";
704
1296
  function readSettings() {
705
1297
  try {
@@ -716,8 +1308,15 @@ function readSettings() {
716
1308
  pick("grouping", (value) => typeof value === "boolean" ? value : void 0);
717
1309
  pick("groupDefault", (value) => value === "expanded" || value === "collapsed" ? value : void 0);
718
1310
  pick("showMeta", (value) => typeof value === "boolean" ? value : void 0);
719
- pick("order", (value) => value === "host" || value === "title" || value === "bytes" ? value : void 0);
1311
+ pick(
1312
+ "order",
1313
+ (value) => value === "host" || value === "title" || value === "bytes" || value === "createdAt" || value === "deletedAt" ? value : void 0
1314
+ );
720
1315
  pick("autoUpdate", (value) => typeof value === "boolean" ? value : void 0);
1316
+ pick("search", (value) => typeof value === "string" ? value.slice(0, SEARCH_MAX_LENGTH) : void 0);
1317
+ pick("timeFilter", (value) => value === "all" || value === "recent" ? value : void 0);
1318
+ pick("trashDays", (value) => typeof value === "number" && TRASH_RETENTION_DAYS.includes(value) ? value : void 0);
1319
+ pick("trashDaysCustom", (value) => typeof value === "string" ? value.slice(0, 8) : void 0);
721
1320
  return next;
722
1321
  } catch {
723
1322
  return { ...DEFAULT_SETTINGS };
@@ -726,6 +1325,7 @@ function readSettings() {
726
1325
  var settingsStore = createStore(readSettings());
727
1326
  function updateSettings(patch) {
728
1327
  const next = { ...settingsStore.get(), ...patch };
1328
+ if (typeof next.search === "string" && next.search.length > SEARCH_MAX_LENGTH) next.search = next.search.slice(0, SEARCH_MAX_LENGTH);
729
1329
  settingsStore.set(next);
730
1330
  try {
731
1331
  if (globalThis.localStorage) globalThis.localStorage.setItem(SETTINGS_KEY, JSON.stringify(next));
@@ -772,19 +1372,64 @@ async function checkUpdate(force) {
772
1372
  }
773
1373
  }
774
1374
 
775
- // src/client/components/group-header.ts
776
- var React3 = __toESM(require("react"), 1);
1375
+ // src/client/components/dropdown.ts
1376
+ var React4 = __toESM(require("react"), 1);
777
1377
  var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
1378
+ function Dropdown(props) {
1379
+ const current2 = props.options.find((option) => option.value === props.value);
1380
+ const display = props.display ?? (current2 === void 0 ? String(props.value) : current2.label);
1381
+ const optionId = (value) => `${props.id}-${String(value)}`;
1382
+ return React4.createElement(import_dsh_client_ui_primitives2.Menu, {
1383
+ open: props.open,
1384
+ portal: true,
1385
+ side: "bottom",
1386
+ align: props.align ?? "end",
1387
+ items: props.options.map((option) => ({
1388
+ id: optionId(option.value),
1389
+ label: option.label,
1390
+ disabled: option.disabled === true
1391
+ })),
1392
+ // 命中当前档位时由 `Menu` 在行**尾**画对勾 —— 这是数据行选中态的唯一来源。
1393
+ selectedId: optionId(props.value),
1394
+ onSelect: (id) => {
1395
+ props.onToggle(false);
1396
+ const picked = props.options.find((option) => optionId(option.value) === id);
1397
+ if (picked !== void 0 && picked.value !== props.value) props.onChange(picked.value);
1398
+ },
1399
+ onClose: () => props.onToggle(false),
1400
+ className: S.dropdownAnchor,
1401
+ anchor: React4.createElement(
1402
+ import_dsh_client_ui_primitives2.Button,
1403
+ {
1404
+ size: "sm",
1405
+ variant: "outline",
1406
+ disabled: props.disabled === true,
1407
+ style: S.dropdownButton,
1408
+ "aria-label": props.label,
1409
+ "aria-haspopup": "menu",
1410
+ "aria-expanded": props.open,
1411
+ icon: React4.createElement(import_dsh_client_ui_primitives2.IconChevronsUpDownOutlineRegular, { size: 14 }),
1412
+ onClick: () => props.onToggle(!props.open)
1413
+ },
1414
+ display
1415
+ )
1416
+ });
1417
+ }
1418
+
1419
+ // src/client/components/group-header.ts
1420
+ var React5 = __toESM(require("react"), 1);
1421
+ var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
778
1422
  function GroupHeader(props) {
779
1423
  const group = props.group;
780
1424
  const t = props.t;
781
1425
  const total = group.rows.length;
782
1426
  const allSelected = total > 0 && group.rows.every((row) => props.selected.includes(row.sessionId));
783
1427
  const action = allSelected ? t("clearShort") : t("selectAllShort");
784
- return React3.createElement(
1428
+ const workspaceActions = props.workspaceActions ?? [];
1429
+ return React5.createElement(
785
1430
  "div",
786
1431
  { style: props.first ? { ...S.groupHead, borderTop: "none" } : S.groupHead },
787
- React3.createElement(
1432
+ React5.createElement(
788
1433
  "button",
789
1434
  {
790
1435
  type: "button",
@@ -792,28 +1437,78 @@ function GroupHeader(props) {
792
1437
  title: props.collapsed ? t("expand") : t("collapse"),
793
1438
  onClick: () => props.onToggleCollapse()
794
1439
  },
795
- React3.createElement("span", { style: S.groupCaret }, props.collapsed ? React3.createElement(import_dsh_client_ui_primitives2.IconChevronRightOutlineRegular, { size: 12 }) : React3.createElement(import_dsh_client_ui_primitives2.IconChevronDownOutlineRegular, { size: 12 })),
796
- React3.createElement("span", { style: S.groupIcon }, props.collapsed ? React3.createElement(import_dsh_client_ui_primitives2.IconFolderCloseRegular, { size: 14 }) : React3.createElement(import_dsh_client_ui_primitives2.IconFolderOpenRegular, { size: 14 })),
797
- React3.createElement("span", { style: S.groupTitle, title: group.cwd || group.title }, group.title),
798
- React3.createElement("span", { style: S.groupMeta }, text(t, "sessions", { n: total })),
799
- group.bytes > 0 ? React3.createElement("span", { style: S.groupMeta }, formatBytes(group.bytes)) : null,
800
- group.cwd !== null ? React3.createElement("span", { style: { ...S.groupPath, ...S.mono }, title: group.cwd }, group.cwd) : null
1440
+ React5.createElement(
1441
+ "span",
1442
+ { style: S.groupCaret },
1443
+ props.collapsed ? React5.createElement(import_dsh_client_ui_primitives3.IconChevronRightOutlineRegular, { size: 12 }) : React5.createElement(import_dsh_client_ui_primitives3.IconChevronDownOutlineRegular, { size: 12 })
1444
+ ),
1445
+ React5.createElement(
1446
+ "span",
1447
+ { style: S.groupIcon },
1448
+ props.collapsed ? React5.createElement(import_dsh_client_ui_primitives3.IconFolderCloseRegular, { size: 14 }) : React5.createElement(import_dsh_client_ui_primitives3.IconFolderOpenRegular, { size: 14 })
1449
+ ),
1450
+ React5.createElement("span", { style: S.groupTitle, title: group.cwd || group.title }, group.title),
1451
+ React5.createElement("span", { style: S.groupMeta }, text(t, "sessions", { n: total })),
1452
+ group.bytes > 0 ? React5.createElement("span", { style: S.groupMeta }, formatBytes(group.bytes)) : null,
1453
+ group.cwd !== null ? React5.createElement("span", { style: { ...S.groupPath, ...S.mono }, title: group.cwd }, group.cwd) : null
801
1454
  ),
802
- React3.createElement(import_dsh_client_ui_primitives2.Button, {
803
- size: "sm",
804
- variant: "ghost",
805
- disabled: props.busy,
806
- // 长路径不许把这个按钮挤窄(否则中文标签折行),见 styles.ts 的 groupAction。
807
- style: S.groupAction,
808
- title: `${group.title} — ${allSelected ? t("clearGroup") : t("selectGroup")}`,
809
- onClick: () => props.onToggleGroup()
810
- }, action)
1455
+ // 工作区动作:先勾选(本页签),再「⋯」(跨页签)。两个都不参与 flex 收缩,
1456
+ // 长路径挤的只能是左边的折叠按钮(见 styles.ts 的 groupAction)。
1457
+ React5.createElement(
1458
+ import_dsh_client_ui_primitives3.Button,
1459
+ {
1460
+ size: "sm",
1461
+ variant: "ghost",
1462
+ disabled: props.busy,
1463
+ style: S.groupAction,
1464
+ title: `${group.title} — ${allSelected ? t("clearGroup") : t("selectGroup")}`,
1465
+ onClick: () => props.onToggleGroup()
1466
+ },
1467
+ action
1468
+ ),
1469
+ workspaceActions.length === 0 ? null : React5.createElement(import_dsh_client_ui_primitives3.Menu, {
1470
+ open: props.workspaceMenuOpen === true,
1471
+ portal: true,
1472
+ side: "bottom",
1473
+ align: "end",
1474
+ closeOnPointerLeave: true,
1475
+ items: workspaceActions,
1476
+ onSelect: (id) => props.onRunWorkspaceAction?.(id),
1477
+ onClose: () => props.onCloseWorkspaceMenu?.(),
1478
+ anchor: React5.createElement(import_dsh_client_ui_primitives3.Button, {
1479
+ size: "sm",
1480
+ variant: "ghost",
1481
+ disabled: props.busy,
1482
+ style: S.rowMenuButton,
1483
+ icon: React5.createElement(import_dsh_client_ui_primitives3.IconEllipsisOutlineRegular, {}),
1484
+ "aria-label": `${group.title} — ${t("workspaceActions")}`,
1485
+ onClick: () => props.onToggleWorkspaceMenu?.()
1486
+ })
1487
+ })
811
1488
  );
812
1489
  }
1490
+ function workspaceActionsOf(t, summary) {
1491
+ return [
1492
+ {
1493
+ id: "archive",
1494
+ label: t("archiveWorkspace"),
1495
+ icon: React5.createElement(import_dsh_client_ui_primitives3.IconArchiveOutlineRegular, { size: 14 }),
1496
+ // 全都归档过了就没什么可归档的;有会话正在跑时也不给点(归档会先停掉它的工作)。
1497
+ disabled: summary.archivableIds.length === 0 || summary.activeIds.length > 0
1498
+ },
1499
+ {
1500
+ id: "trash",
1501
+ label: t("trashWorkspace"),
1502
+ icon: React5.createElement(import_dsh_client_ui_primitives3.IconTrashOutlineRegular, { size: 14 }),
1503
+ danger: true,
1504
+ disabled: summary.trashableIds.length === 0 || summary.activeIds.length > 0
1505
+ }
1506
+ ];
1507
+ }
813
1508
 
814
1509
  // src/client/components/session-row.ts
815
- var React4 = __toESM(require("react"), 1);
816
- var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
1510
+ var React6 = __toESM(require("react"), 1);
1511
+ var import_dsh_client_ui_primitives4 = require("@deepseek-ai/dsh-client-ui-primitives");
817
1512
  function SessionRow(props) {
818
1513
  const row = props.row;
819
1514
  const t = props.t;
@@ -826,26 +1521,33 @@ function SessionRow(props) {
826
1521
  const time = props.showDeletedAt ? formatTime(row.deletedAt) : formatTime(row.createdAt);
827
1522
  if (time !== "") meta.push(time);
828
1523
  const menu = props.menu;
829
- return React4.createElement(
1524
+ const running = row.active === true;
1525
+ return React6.createElement(
830
1526
  "div",
831
1527
  { style: props.first ? { ...S.item, borderTop: "none" } : S.item },
832
- React4.createElement("input", {
1528
+ React6.createElement("input", {
833
1529
  type: "checkbox",
834
1530
  checked: props.checked,
835
1531
  disabled: props.busy,
836
1532
  "aria-label": row.title,
837
1533
  onChange: () => props.onToggle()
838
1534
  }),
839
- React4.createElement(
1535
+ React6.createElement(
840
1536
  "div",
841
1537
  { style: S.itemMain },
842
- React4.createElement("div", { style: S.itemTitle, title: row.title }, row.title),
843
- React4.createElement("div", { style: S.itemMeta, title: row.cwd || "" }, meta.join(" · "))
1538
+ React6.createElement(
1539
+ "div",
1540
+ { style: S.itemTitleRow },
1541
+ React6.createElement("div", { style: S.itemTitle, title: row.title }, row.title),
1542
+ // 胶囊:`flex: 0 0 auto`,所以长标题只会挤标题那一段(它有 minWidth: 0 + 省略号)。
1543
+ running ? React6.createElement("span", { style: S.badgeRunning, title: t("runningHint") }, t("running")) : null
1544
+ ),
1545
+ React6.createElement("div", { style: S.itemMeta, title: row.cwd || "" }, meta.join(" · "))
844
1546
  ),
845
- React4.createElement(
1547
+ React6.createElement(
846
1548
  "div",
847
1549
  { style: S.itemActions },
848
- React4.createElement(import_dsh_client_ui_primitives3.Menu, {
1550
+ React6.createElement(import_dsh_client_ui_primitives4.Menu, {
849
1551
  open: props.menuOpen,
850
1552
  portal: true,
851
1553
  side: "bottom",
@@ -854,12 +1556,12 @@ function SessionRow(props) {
854
1556
  items: menu.items,
855
1557
  onSelect: (id) => menu.run(id),
856
1558
  onClose: () => props.onCloseMenu(),
857
- anchor: React4.createElement(import_dsh_client_ui_primitives3.Button, {
1559
+ anchor: React6.createElement(import_dsh_client_ui_primitives4.Button, {
858
1560
  size: "sm",
859
1561
  variant: "ghost",
860
1562
  disabled: props.busy,
861
1563
  style: S.rowMenuButton,
862
- icon: React4.createElement(import_dsh_client_ui_primitives3.IconEllipsisOutlineRegular, {}),
1564
+ icon: React6.createElement(import_dsh_client_ui_primitives4.IconEllipsisOutlineRegular, {}),
863
1565
  "aria-label": `${row.title} — ${t("rowActions")}`,
864
1566
  onClick: (event) => {
865
1567
  event.stopPropagation();
@@ -871,27 +1573,149 @@ function SessionRow(props) {
871
1573
  );
872
1574
  }
873
1575
 
1576
+ // src/client/components/usage-panel.ts
1577
+ var React7 = __toESM(require("react"), 1);
1578
+ var import_dsh_client_ui_primitives5 = require("@deepseek-ai/dsh-client-ui-primitives");
1579
+ function usageRow(t, row, busy, onArchive, onUnarchive) {
1580
+ const badges = [];
1581
+ if (row.trashed) badges.push({ key: "trashed", label: t("usageTrashed"), style: S.badgeDanger });
1582
+ else if (row.archived) badges.push({ key: "archived", label: t("usageArchived"), style: S.badge });
1583
+ else if (row.pinned) badges.push({ key: "pinned", label: t("usagePinned"), style: S.badge });
1584
+ if (row.active) badges.push({ key: "running", label: t("running"), style: S.badgeRunning });
1585
+ const action = row.trashed ? null : row.archived ? { label: t("usageUnarchive"), run: () => onUnarchive(row.sessionId) } : row.active ? null : { label: t("usageArchive"), run: () => onArchive(row.sessionId) };
1586
+ const when = row.deletedAt !== null ? formatTime(row.deletedAt) : formatTime(row.createdAt);
1587
+ return React7.createElement(
1588
+ "div",
1589
+ { key: row.sessionId, style: S.usageRow },
1590
+ React7.createElement(
1591
+ "div",
1592
+ { style: S.itemMain },
1593
+ React7.createElement(
1594
+ "div",
1595
+ { style: S.itemTitleRow },
1596
+ React7.createElement("div", { style: S.itemTitle, title: row.title }, row.title),
1597
+ badges.map((badge) => React7.createElement("span", { key: badge.key, style: badge.style }, badge.label))
1598
+ ),
1599
+ React7.createElement(
1600
+ "div",
1601
+ { style: S.itemMeta, title: row.cwd || "" },
1602
+ [formatBytes(row.bytes), text(t, "files", { n: row.files }), row.workspaceTitle ?? row.cwd ?? t("noProject"), when].filter((part) => part !== "").join(" · ")
1603
+ )
1604
+ ),
1605
+ React7.createElement(
1606
+ "div",
1607
+ { style: S.itemActions },
1608
+ action === null ? null : React7.createElement(
1609
+ import_dsh_client_ui_primitives5.Button,
1610
+ {
1611
+ size: "sm",
1612
+ variant: "outline",
1613
+ disabled: busy,
1614
+ onClick: action.run
1615
+ },
1616
+ action.label
1617
+ )
1618
+ )
1619
+ );
1620
+ }
1621
+ function UsagePanel(props) {
1622
+ const t = props.t;
1623
+ const usage = props.usage;
1624
+ const maxBytes = usage === null || usage.groups.length === 0 ? 0 : Math.max(...usage.groups.map((group) => group.bytes), 1);
1625
+ return React7.createElement(
1626
+ "div",
1627
+ { style: S.card },
1628
+ React7.createElement(
1629
+ "div",
1630
+ { style: { ...S.configRow, borderTop: "none" } },
1631
+ React7.createElement(
1632
+ "div",
1633
+ { style: S.configMain },
1634
+ React7.createElement("span", { style: S.configLabel }, t("usageTitle")),
1635
+ React7.createElement(
1636
+ "span",
1637
+ { style: S.configHint },
1638
+ props.loading ? t("usageLoading") : usage === null ? props.error ?? "" : text(t, "usageTotal", {
1639
+ n: usage.totalSessions,
1640
+ size: formatBytes(usage.totalBytes),
1641
+ disk: usage.onDiskSessions
1642
+ })
1643
+ )
1644
+ )
1645
+ ),
1646
+ usage === null || usage.totalSessions === 0 ? React7.createElement("div", { style: S.empty }, props.loading ? t("usageLoading") : t("usageNoData")) : React7.createElement(
1647
+ "div",
1648
+ { style: S.usageBody },
1649
+ // 按工作区:横条 + 名字 + 体积 / 条数。
1650
+ React7.createElement("span", { style: S.usageHeading }, t("usageByWorkspace")),
1651
+ usage.groups.map(
1652
+ (group) => React7.createElement(
1653
+ "div",
1654
+ { key: group.key, style: S.usageBarRow },
1655
+ React7.createElement(
1656
+ "span",
1657
+ { style: S.usageBarTrack },
1658
+ React7.createElement("span", {
1659
+ style: {
1660
+ ...S.usageBarFill,
1661
+ // 占比:按最大组归一,所以最小的组也看得见(下限 2%)。
1662
+ width: `${Math.max(2, Math.round(group.bytes / maxBytes * 100))}%`
1663
+ }
1664
+ })
1665
+ ),
1666
+ React7.createElement(
1667
+ "span",
1668
+ { style: S.usageBarName, title: group.key },
1669
+ group.title === "" ? t("noProject") : group.title
1670
+ ),
1671
+ React7.createElement(
1672
+ "span",
1673
+ { style: S.usageBarMeta },
1674
+ `${formatBytes(group.bytes)} · ${text(t, "sessions", { n: group.count })}`
1675
+ )
1676
+ )
1677
+ ),
1678
+ React7.createElement("span", { style: S.usageHeading }, t("usageLargest")),
1679
+ usage.largest.map((row) => usageRow(t, row, props.busy, props.onArchive, props.onUnarchive)),
1680
+ React7.createElement("span", { style: S.usageHeading }, t("usageOldest")),
1681
+ usage.oldest.map((row) => usageRow(t, row, props.busy, props.onArchive, props.onUnarchive))
1682
+ )
1683
+ );
1684
+ }
1685
+
874
1686
  // src/client/components/settings-panel.ts
875
1687
  function SessionManagerPanel(props) {
876
1688
  const t = props.t;
877
1689
  const settings = useStore(settingsStore);
878
- const [tab, setTab] = React5.useState(settings.tab);
879
- const [snapshot, setSnapshot] = React5.useState(null);
880
- const [error, setError] = React5.useState(null);
881
- const [loading, setLoading] = React5.useState(true);
882
- const [busy, setBusy] = React5.useState(false);
883
- const [selected, setSelected] = React5.useState([]);
884
- const [collapsedOverrides, setCollapsedOverrides] = React5.useState({});
885
- const [menuFor, setMenuFor] = React5.useState(null);
1690
+ const [tab, setTab] = React8.useState(settings.tab);
1691
+ const [snapshot, setSnapshot] = React8.useState(cachedSnapshot);
1692
+ const [error, setError] = React8.useState(null);
1693
+ const [loading, setLoading] = React8.useState(true);
1694
+ const [busy, setBusy] = React8.useState(false);
1695
+ const [selected, setSelected] = React8.useState([]);
1696
+ const [collapsedOverrides, setCollapsedOverrides] = React8.useState({});
1697
+ const [menuFor, setMenuFor] = React8.useState(null);
1698
+ const [workspaceMenuFor, setWorkspaceMenuFor] = React8.useState(null);
1699
+ const [trashStats, setTrashStats] = React8.useState(null);
1700
+ const [trashStatsError, setTrashStatsError] = React8.useState(null);
1701
+ const [trashStatsBusy, setTrashStatsBusy] = React8.useState(false);
1702
+ const [usage, setUsage] = React8.useState(null);
1703
+ const [usageError, setUsageError] = React8.useState(null);
1704
+ const [usageBusy, setUsageBusy] = React8.useState(false);
1705
+ const [usageReload, setUsageReload] = React8.useState(0);
1706
+ const [openDropdown, setOpenDropdown] = React8.useState(null);
886
1707
  const grouping = useStore(groupingStore);
887
1708
  const revision = useStore(mutationStore);
888
1709
  const update = useStore(updateStore);
889
- const [sidebarArchived, setSidebarArchived] = React5.useState(readSidebarArchived);
890
- const [sidebarAtMount] = React5.useState(readSidebarArchived);
1710
+ const [sidebarArchived, setSidebarArchived] = React8.useState(readSidebarArchived);
1711
+ const [sidebarAtMount] = React8.useState(readSidebarArchived);
891
1712
  const applyResult = (value) => {
892
- if (value && value.archived) setSnapshot(value);
1713
+ if (value && value.archived) {
1714
+ rememberSnapshot(value);
1715
+ setSnapshot(value);
1716
+ }
893
1717
  };
894
- const refresh = React5.useCallback(async () => {
1718
+ const refresh = React8.useCallback(async () => {
895
1719
  setLoading(true);
896
1720
  try {
897
1721
  applyResult(await call("snapshot", {}));
@@ -902,17 +1726,61 @@ function SessionManagerPanel(props) {
902
1726
  setLoading(false);
903
1727
  }
904
1728
  }, []);
905
- React5.useEffect(() => {
1729
+ React8.useEffect(() => {
906
1730
  void refresh();
907
1731
  }, [refresh, revision]);
908
- React5.useEffect(() => {
1732
+ React8.useEffect(() => {
909
1733
  if (tab !== "config" || !settings.autoUpdate || update.state !== "idle") return;
910
1734
  void checkUpdate(false);
911
1735
  }, [tab, settings.autoUpdate, update.state]);
912
- React5.useEffect(() => {
1736
+ React8.useEffect(() => {
913
1737
  setSelected([]);
914
1738
  setMenuFor(null);
1739
+ setWorkspaceMenuFor(null);
915
1740
  }, [tab]);
1741
+ const retentionDays = effectiveTrashDays(settings);
1742
+ React8.useEffect(() => {
1743
+ if (tab !== "config") return;
1744
+ let cancelled = false;
1745
+ setTrashStatsBusy(true);
1746
+ setTrashStatsError(null);
1747
+ void call("trash-stats", { days: retentionDays }).then((value) => {
1748
+ if (!cancelled) setTrashStats(value);
1749
+ }).catch((failure) => {
1750
+ if (!cancelled) {
1751
+ setTrashStats(null);
1752
+ setTrashStatsError(reportError(failure));
1753
+ }
1754
+ }).finally(() => {
1755
+ if (!cancelled) setTrashStatsBusy(false);
1756
+ });
1757
+ return () => {
1758
+ cancelled = true;
1759
+ };
1760
+ }, [tab, retentionDays]);
1761
+ React8.useEffect(() => {
1762
+ if (tab !== "usage") return;
1763
+ let cancelled = false;
1764
+ setUsageBusy(true);
1765
+ setUsageError(null);
1766
+ void call("usage", {}).then((value) => {
1767
+ if (!cancelled) setUsage(value);
1768
+ }).catch((failure) => {
1769
+ if (!cancelled) {
1770
+ setUsage(null);
1771
+ setUsageError(reportError(failure));
1772
+ }
1773
+ }).finally(() => {
1774
+ if (!cancelled) setUsageBusy(false);
1775
+ });
1776
+ return () => {
1777
+ cancelled = true;
1778
+ };
1779
+ }, [tab, revision, usageReload]);
1780
+ const refreshUsage = () => setUsageReload((current2) => current2 + 1);
1781
+ const runUsageAction = (endpoint, sessionId) => {
1782
+ void run(endpoint, [sessionId]).then(() => refreshUsage());
1783
+ };
916
1784
  const run = async (endpoint, ids) => {
917
1785
  if (ids.length === 0) return;
918
1786
  setBusy(true);
@@ -942,35 +1810,92 @@ function SessionManagerPanel(props) {
942
1810
  const pinned = snapshot ? snapshot.pinned : [];
943
1811
  const trashed = snapshot ? snapshot.trashed : [];
944
1812
  const rows = sortRows(tab === "archived" ? archived : tab === "pinned" ? pinned : tab === "trash" ? trashed : [], settings.order);
1813
+ const filter = React8.useMemo(
1814
+ () => ({ search: settings.search, timeFilter: settings.timeFilter }),
1815
+ [settings.search, settings.timeFilter]
1816
+ );
1817
+ const filtered = React8.useMemo(() => filterRows(rows, filter), [rows, filter]);
1818
+ const filtering = isFilterActive(filter);
945
1819
  const selectedRows = rows.filter((row) => selected.includes(row.sessionId));
946
- const groups = grouping ? groupRows(rows, t) : [];
1820
+ const groups = grouping ? groupRows(filtered, t) : [];
947
1821
  const blockOf = (group) => {
948
1822
  const key = collapseKey(tab, group.key);
949
1823
  return { key, group, rows: isGroupCollapsed(key, collapsedOverrides, settings.groupDefault) ? [] : group.rows };
950
1824
  };
951
- const blocks = grouping ? groups.map(blockOf) : [{ key: `${tab}:flat`, group: null, rows }];
1825
+ const blocks = grouping ? groups.map(blockOf) : [{ key: `${tab}:flat`, group: null, rows: filtered }];
952
1826
  const tabs = [
953
1827
  { id: "archived", label: t(TAB_LABEL.archived), count: archived.length },
954
1828
  { id: "pinned", label: t(TAB_LABEL.pinned), count: pinned.length },
955
1829
  { id: "trash", label: t(TAB_LABEL.trash), count: trashed.length },
1830
+ { id: "usage", label: t("titleUsage"), count: 0 },
956
1831
  { id: "config", label: t("titleConfig"), count: 0 }
957
1832
  ];
1833
+ const orderOptions = [
1834
+ { value: "host", label: t("orderHost") },
1835
+ { value: "title", label: t("orderTitle") },
1836
+ { value: "bytes", label: t("orderBytes") },
1837
+ { value: "createdAt", label: t("orderCreatedAt") },
1838
+ { value: "deletedAt", label: t("orderDeletedAt") }
1839
+ ];
1840
+ const customDays = settings.trashDaysCustom;
1841
+ const customDaysValid = customDays.trim() === "" || parseCustomDays(customDays) !== null;
1842
+ const retentionOptions = TRASH_RETENTION_DAYS.map((days) => ({ value: days, label: text(t, "retentionDays", { n: days }) }));
1843
+ const summaries = React8.useMemo(() => workspaceSummaries(archived, pinned, trashed), [archived, pinned, trashed]);
1844
+ const copyRowPath = async (sessionId) => {
1845
+ try {
1846
+ const value = await openSessionPath(sessionId);
1847
+ const copied = await copyText(value.path);
1848
+ showNotice(copied ? text(t, "pathCopied", { path: value.path }) : text(t, "pathCopyFailed", { path: value.path }));
1849
+ } catch (failure) {
1850
+ showNotice(reportError(failure));
1851
+ }
1852
+ };
1853
+ const revealRow = async (sessionId) => {
1854
+ try {
1855
+ const value = await openSessionPath(sessionId, true);
1856
+ if (!value.revealed) {
1857
+ await copyText(value.path);
1858
+ showNotice(text(t, "revealFailed", { reason: value.reason ?? "?", path: value.path }));
1859
+ }
1860
+ } catch (failure) {
1861
+ showNotice(reportError(failure));
1862
+ }
1863
+ };
958
1864
  const rowMenu = (row) => {
1865
+ const running = row.active === true;
959
1866
  const moveToTrash = {
960
1867
  id: "trash",
961
1868
  label: t("moveToTrash"),
962
- icon: React5.createElement(import_dsh_client_ui_primitives4.IconTrashOutlineRegular, { size: 14 }),
963
- danger: true
1869
+ icon: React8.createElement(import_dsh_client_ui_primitives6.IconTrashOutlineRegular, { size: 14 }),
1870
+ danger: true,
1871
+ disabled: running
1872
+ };
1873
+ const locate = [
1874
+ { id: "copy-path", label: t("copyPath"), icon: React8.createElement(import_dsh_client_ui_primitives6.IconCopyOutlineRegular, { size: 14 }) },
1875
+ { id: "reveal", label: t("revealInFolder"), icon: React8.createElement(import_dsh_client_ui_primitives6.IconFolderOpenRegular, { size: 14 }) }
1876
+ ];
1877
+ const handleLocate = (id) => {
1878
+ if (id === "copy-path") {
1879
+ void copyRowPath(row.sessionId);
1880
+ return true;
1881
+ }
1882
+ if (id === "reveal") {
1883
+ void revealRow(row.sessionId);
1884
+ return true;
1885
+ }
1886
+ return false;
964
1887
  };
965
1888
  if (tab === "archived") {
966
1889
  return {
967
1890
  items: [
968
- { id: "unarchive", label: t("unarchive"), icon: React5.createElement(import_dsh_client_ui_primitives4.IconUnarchiveOutlineRegular, { size: 14 }) },
969
- { id: "pin", label: t("pin"), icon: React5.createElement(import_dsh_client_ui_primitives4.IconPinOutlineRegular, { size: 14 }) },
970
- moveToTrash
1891
+ { id: "unarchive", label: t("unarchive"), icon: React8.createElement(import_dsh_client_ui_primitives6.IconUnarchiveOutlineRegular, { size: 14 }) },
1892
+ { id: "pin", label: t("pin"), icon: React8.createElement(import_dsh_client_ui_primitives6.IconPinOutlineRegular, { size: 14 }) },
1893
+ moveToTrash,
1894
+ ...locate
971
1895
  ],
972
1896
  run: (id) => {
973
1897
  setMenuFor(null);
1898
+ if (handleLocate(id)) return;
974
1899
  if (id === "trash") requestTrash([row.sessionId], row.title);
975
1900
  else void run(id, [row.sessionId]);
976
1901
  }
@@ -979,40 +1904,71 @@ function SessionManagerPanel(props) {
979
1904
  if (tab === "pinned") {
980
1905
  return {
981
1906
  items: [
982
- { id: "unpin", label: t("unpin"), icon: React5.createElement(import_dsh_client_ui_primitives4.IconPinOutlineRegular, { size: 14 }) },
983
- moveToTrash
1907
+ { id: "unpin", label: t("unpin"), icon: React8.createElement(import_dsh_client_ui_primitives6.IconPinOutlineRegular, { size: 14 }) },
1908
+ moveToTrash,
1909
+ ...locate
984
1910
  ],
985
1911
  run: (id) => {
986
1912
  setMenuFor(null);
1913
+ if (handleLocate(id)) return;
987
1914
  if (id === "trash") requestTrash([row.sessionId], row.title);
988
1915
  else void run(id, [row.sessionId]);
989
1916
  }
990
1917
  };
991
1918
  }
1919
+ const restorable = row.restorable !== false;
992
1920
  return {
993
1921
  items: [
994
- { id: "restore", label: t("restore"), icon: React5.createElement(import_dsh_client_ui_primitives4.IconRefreshOutlineRegular, { size: 14 }) },
995
- { id: "purge", label: t("purge"), icon: React5.createElement(import_dsh_client_ui_primitives4.IconTrashOutlineRegular, { size: 14 }), danger: true }
1922
+ {
1923
+ id: "restore",
1924
+ label: t(restorable ? "restore" : "restoreUnavailable"),
1925
+ icon: React8.createElement(import_dsh_client_ui_primitives6.IconRefreshOutlineRegular, { size: 14 }),
1926
+ disabled: !restorable
1927
+ },
1928
+ { id: "purge", label: t("purge"), icon: React8.createElement(import_dsh_client_ui_primitives6.IconTrashOutlineRegular, { size: 14 }), danger: true },
1929
+ ...locate
996
1930
  ],
997
1931
  run: (id) => {
998
1932
  setMenuFor(null);
1933
+ if (handleLocate(id)) return;
1934
+ if (id === "restore" && !restorable) return;
999
1935
  if (id === "purge") requestPurge([row.sessionId], row.title);
1000
1936
  else void run(id, [row.sessionId]);
1001
1937
  }
1002
1938
  };
1003
1939
  };
1940
+ const runWorkspaceAction = (group, id) => {
1941
+ setWorkspaceMenuFor(null);
1942
+ const summary = summaries.get(group.key);
1943
+ if (summary === void 0) return;
1944
+ if (id === "archive") void run("archive", summary.archivableIds);
1945
+ if (id === "trash") requestTrash(summary.trashableIds, group.title);
1946
+ };
1004
1947
  const batch = [];
1005
1948
  if (tab === "archived" && selected.length > 0) {
1006
1949
  batch.push({ label: t("unarchive"), run: () => void run("unarchive", selected) });
1007
- batch.push({ label: t("moveToTrash"), variant: "outline", run: () => requestTrash(selected, selectedRows[0] ? selectedRows[0].title : "") });
1950
+ batch.push({
1951
+ label: t("moveToTrash"),
1952
+ variant: "outline",
1953
+ run: () => requestTrash(selected, selectedRows[0] ? selectedRows[0].title : "")
1954
+ });
1008
1955
  }
1009
1956
  if (tab === "pinned" && selected.length > 0) {
1010
1957
  batch.push({ label: t("unpin"), run: () => void run("unpin", selected) });
1011
- batch.push({ label: t("moveToTrash"), variant: "outline", run: () => requestTrash(selected, selectedRows[0] ? selectedRows[0].title : "") });
1958
+ batch.push({
1959
+ label: t("moveToTrash"),
1960
+ variant: "outline",
1961
+ run: () => requestTrash(selected, selectedRows[0] ? selectedRows[0].title : "")
1962
+ });
1012
1963
  }
1013
1964
  if (tab === "trash" && selected.length > 0) {
1014
- batch.push({ label: t("restore"), run: () => void run("restore", selected) });
1015
- batch.push({ label: t("purge"), variant: "outline", run: () => requestPurge(selected, selectedRows[0] ? selectedRows[0].title : "") });
1965
+ const canRestore = selectedRows.some((row) => row.restorable !== false);
1966
+ batch.push({ label: t("restore"), disabled: !canRestore, run: () => void run("restore", selected) });
1967
+ batch.push({
1968
+ label: t("purge"),
1969
+ variant: "outline",
1970
+ run: () => requestPurge(selected, selectedRows[0] ? selectedRows[0].title : "")
1971
+ });
1016
1972
  }
1017
1973
  const totals = snapshot ? snapshot.totals : null;
1018
1974
  const totalBytes = tab === "archived" ? totals ? totals.archivedBytes : 0 : tab === "trash" ? totals ? totals.trashBytes : 0 : 0;
@@ -1022,272 +1978,580 @@ function SessionManagerPanel(props) {
1022
1978
  if (block.group !== null) {
1023
1979
  const group = block.group;
1024
1980
  const key = block.key;
1025
- nodes.push(React5.createElement(GroupHeader, {
1026
- key: `group:${key}`,
1027
- group,
1028
- t,
1029
- first,
1030
- busy,
1031
- selected,
1032
- collapsed: isGroupCollapsed(key, collapsedOverrides, settings.groupDefault),
1033
- onToggleGroup: () => toggleGroup(group),
1034
- onToggleCollapse: () => toggleCollapse(key)
1035
- }));
1981
+ const summary = summaries.get(group.key);
1982
+ nodes.push(
1983
+ React8.createElement(GroupHeader, {
1984
+ key: `group:${key}`,
1985
+ group,
1986
+ t,
1987
+ first,
1988
+ busy,
1989
+ selected,
1990
+ collapsed: isGroupCollapsed(key, collapsedOverrides, settings.groupDefault),
1991
+ onToggleGroup: () => toggleGroup(group),
1992
+ onToggleCollapse: () => toggleCollapse(key),
1993
+ // 「整个工作区」:跨页签的目标集合由 `workspaceSummaries()` 算好。
1994
+ // "未知目录"那一组也有 key,但它的目标集合可能只含回收站条目 ——
1995
+ // `workspaceActionsOf` 会把两项都置 `disabled`,菜单仍然画得出来(不是坏形状)。
1996
+ workspaceActions: summary === void 0 ? [] : workspaceActionsOf(t, summary),
1997
+ workspaceMenuOpen: workspaceMenuFor === group.key,
1998
+ onToggleWorkspaceMenu: () => setWorkspaceMenuFor(workspaceMenuFor === group.key ? null : group.key),
1999
+ onCloseWorkspaceMenu: () => setWorkspaceMenuFor(null),
2000
+ onRunWorkspaceAction: (id) => runWorkspaceAction(group, id)
2001
+ })
2002
+ );
1036
2003
  first = false;
1037
2004
  }
1038
2005
  for (const row of block.rows) {
1039
- nodes.push(React5.createElement(SessionRow, {
1040
- key: row.sessionId,
1041
- row,
1042
- t,
1043
- first,
1044
- busy,
1045
- showCwd: block.group === null,
1046
- showMeta: settings.showMeta,
1047
- showDeletedAt: tab === "trash",
1048
- checked: selected.includes(row.sessionId),
1049
- onToggle: () => toggle(row.sessionId),
1050
- menu: rowMenu(row),
1051
- menuOpen: menuFor === row.sessionId,
1052
- onToggleMenu: () => setMenuFor(menuFor === row.sessionId ? null : row.sessionId),
1053
- onCloseMenu: () => setMenuFor(null)
1054
- }));
2006
+ nodes.push(
2007
+ React8.createElement(SessionRow, {
2008
+ key: row.sessionId,
2009
+ row,
2010
+ t,
2011
+ first,
2012
+ busy,
2013
+ showCwd: block.group === null,
2014
+ showMeta: settings.showMeta,
2015
+ showDeletedAt: tab === "trash",
2016
+ checked: selected.includes(row.sessionId),
2017
+ onToggle: () => toggle(row.sessionId),
2018
+ menu: rowMenu(row),
2019
+ menuOpen: menuFor === row.sessionId,
2020
+ onToggleMenu: () => setMenuFor(menuFor === row.sessionId ? null : row.sessionId),
2021
+ onCloseMenu: () => setMenuFor(null)
2022
+ })
2023
+ );
1055
2024
  first = false;
1056
2025
  }
1057
2026
  }
1058
2027
  const isConfig = tab === "config";
1059
- const configRow = (key, label, hint, control, firstRow) => React5.createElement(
2028
+ const isUsage = tab === "usage";
2029
+ const isDataTab = !isConfig && !isUsage;
2030
+ const configRow = (key, label, hint, control, firstRow) => React8.createElement(
1060
2031
  "div",
1061
2032
  { key, style: firstRow ? { ...S.configRow, borderTop: "none" } : S.configRow },
1062
- React5.createElement(
2033
+ React8.createElement(
1063
2034
  "div",
1064
2035
  { style: S.configMain },
1065
- React5.createElement("span", { style: S.configLabel }, label),
1066
- hint === null ? null : React5.createElement("span", { style: S.configHint }, hint)
2036
+ React8.createElement("span", { style: S.configLabel }, label),
2037
+ hint === null ? null : React8.createElement("span", { style: S.configHint }, hint)
1067
2038
  ),
1068
- React5.createElement("div", { style: S.configControl }, control)
2039
+ React8.createElement("div", { style: S.configControl }, control)
2040
+ );
2041
+ const retentionCount = trashStats === null ? 0 : trashStats.count;
2042
+ const retentionBytes = trashStats === null ? 0 : trashStats.bytes;
2043
+ const retentionFoot = React8.createElement(
2044
+ "div",
2045
+ { key: "config:trash-preview", style: S.configFoot },
2046
+ trashStatsBusy ? React8.createElement("span", { style: S.configHint }, t("trashPreviewLoading")) : trashStatsError !== null ? React8.createElement("span", { style: S.error }, trashStatsError) : React8.createElement(
2047
+ "span",
2048
+ { style: S.configHint },
2049
+ retentionCount > 0 ? text(t, "trashPreview", { n: retentionCount, size: formatBytes(retentionBytes) }) : text(t, "trashPreviewNone", { days: retentionDays })
2050
+ ),
2051
+ React8.createElement(
2052
+ import_dsh_client_ui_primitives6.Button,
2053
+ {
2054
+ size: "sm",
2055
+ variant: "outline",
2056
+ // 自定义天数非法时不给点:这时候 `retentionDays` 回落到档位,点下去删的和看到的
2057
+ // 那行数字对不上(虽然那行也是按档位算的,但用户以为自己填的数生效了)。
2058
+ disabled: busy || trashStatsBusy || retentionCount === 0 || !customDaysValid,
2059
+ onClick: () => requestPurgeOld(retentionDays, retentionCount, retentionBytes)
2060
+ },
2061
+ text(t, "trashCleanup", { n: retentionDays })
2062
+ )
2063
+ );
2064
+ const filterBar = React8.createElement(
2065
+ "div",
2066
+ { style: S.filterBar },
2067
+ React8.createElement(import_dsh_client_ui_primitives6.Input, {
2068
+ value: settings.search,
2069
+ placeholder: t("searchPlaceholder"),
2070
+ "aria-label": t("searchLabel"),
2071
+ icon: React8.createElement(import_dsh_client_ui_primitives6.IconSearchOutlineRegular, {}),
2072
+ style: S.searchInput,
2073
+ onChange: (event) => void updateSettings({ search: event.target.value })
2074
+ }),
2075
+ filtering ? React8.createElement(
2076
+ import_dsh_client_ui_primitives6.Button,
2077
+ {
2078
+ size: "sm",
2079
+ variant: "ghost",
2080
+ onClick: () => void updateSettings({ search: "", timeFilter: "all" })
2081
+ },
2082
+ t("searchClear")
2083
+ ) : null,
2084
+ React8.createElement("div", { style: S.filterSpacer }),
2085
+ React8.createElement(import_dsh_client_ui_primitives6.SegmentedControl, {
2086
+ id: "session-curator-time-filter",
2087
+ value: settings.timeFilter,
2088
+ options: [
2089
+ { value: "all", label: t("timeAll") },
2090
+ { value: "recent", label: t("timeRecent") }
2091
+ ],
2092
+ label: t("timeFilterLabel"),
2093
+ onChange: (next) => void updateSettings({ timeFilter: next })
2094
+ })
1069
2095
  );
1070
2096
  const version = snapshot !== null && typeof snapshot.version === "string" ? snapshot.version : update.value !== null && typeof update.value.current === "string" ? update.value.current : null;
1071
2097
  const outdated = update.state === "done" && update.value !== null && update.value.outdated === true;
1072
- const updateLine = update.state === "checking" ? React5.createElement("span", { style: S.configHint }, t("updateChecking")) : update.state === "failed" ? React5.createElement("span", { style: S.error }, text(t, "updateFailed", { reason: update.error === null ? "?" : update.error })) : update.value !== null && typeof update.value.error === "string" ? React5.createElement("span", { style: S.configHint }, text(t, "updateFailed", { reason: update.value.error })) : update.state === "done" && !outdated ? React5.createElement("span", { style: S.configHint }, t("updateLatest")) : null;
1073
- const configPage = React5.createElement(
2098
+ const updateLine = update.state === "checking" ? React8.createElement("span", { style: S.configHint }, t("updateChecking")) : update.state === "failed" ? React8.createElement(
2099
+ "span",
2100
+ { style: S.error },
2101
+ text(t, "updateFailed", { reason: update.error === null ? "?" : update.error })
2102
+ ) : update.value !== null && typeof update.value.error === "string" ? React8.createElement("span", { style: S.configHint }, text(t, "updateFailed", { reason: update.value.error })) : update.state === "done" && !outdated ? React8.createElement("span", { style: S.configHint }, t("updateLatest")) : null;
2103
+ const configPage = React8.createElement(
1074
2104
  "div",
1075
2105
  { style: S.column },
1076
- React5.createElement(
2106
+ React8.createElement(
1077
2107
  "div",
1078
2108
  { style: S.card },
1079
- configRow("config:tab", t("configTab"), t("configTabHint"), React5.createElement(import_dsh_client_ui_primitives4.SegmentedControl, {
1080
- id: "session-curator-default-tab",
1081
- value: settings.tab,
1082
- options: [
1083
- { value: "archived", label: t(TAB_LABEL.archived) },
1084
- { value: "pinned", label: t(TAB_LABEL.pinned) },
1085
- { value: "trash", label: t(TAB_LABEL.trash) }
1086
- ],
1087
- label: t("configTab"),
1088
- onChange: (next) => void updateSettings({ tab: next })
1089
- }), true),
1090
- configRow("config:grouping", t("configGrouping"), t("configGroupingHint"), React5.createElement(import_dsh_client_ui_primitives4.Switch, {
1091
- checked: settings.grouping,
1092
- label: t("configGrouping"),
1093
- onChange: (next) => {
1094
- updateSettings({ grouping: next });
1095
- groupingStore.set(next);
1096
- }
1097
- })),
1098
- configRow("config:group-default", t("configGroupDefault"), t("configGroupDefaultHint"), React5.createElement(import_dsh_client_ui_primitives4.SegmentedControl, {
1099
- id: "session-curator-group-default",
1100
- value: settings.groupDefault,
1101
- options: [
1102
- { value: "expanded", label: t("groupExpanded") },
1103
- { value: "collapsed", label: t("groupCollapsed") }
1104
- ],
1105
- label: t("configGroupDefault"),
1106
- onChange: (next) => void updateSettings({ groupDefault: next })
1107
- })),
1108
- configRow("config:sidebar-archived", t("configSidebarArchived"), t("configSidebarArchivedHint"), React5.createElement(import_dsh_client_ui_primitives4.Switch, {
1109
- checked: sidebarArchived !== "default",
1110
- label: t("configSidebarArchived"),
1111
- onChange: (next) => {
1112
- writeSidebarArchived(next);
1113
- setSidebarArchived(next ? "show" : "default");
1114
- }
1115
- })),
2109
+ configRow(
2110
+ "config:tab",
2111
+ t("configTab"),
2112
+ t("configTabHint"),
2113
+ React8.createElement(import_dsh_client_ui_primitives6.SegmentedControl, {
2114
+ id: "session-curator-default-tab",
2115
+ value: settings.tab,
2116
+ options: [
2117
+ { value: "archived", label: t(TAB_LABEL.archived) },
2118
+ { value: "pinned", label: t(TAB_LABEL.pinned) },
2119
+ { value: "trash", label: t(TAB_LABEL.trash) }
2120
+ ],
2121
+ label: t("configTab"),
2122
+ onChange: (next) => void updateSettings({ tab: next })
2123
+ }),
2124
+ true
2125
+ ),
2126
+ configRow(
2127
+ "config:grouping",
2128
+ t("configGrouping"),
2129
+ t("configGroupingHint"),
2130
+ React8.createElement(import_dsh_client_ui_primitives6.Switch, {
2131
+ checked: settings.grouping,
2132
+ label: t("configGrouping"),
2133
+ onChange: (next) => {
2134
+ updateSettings({ grouping: next });
2135
+ groupingStore.set(next);
2136
+ }
2137
+ })
2138
+ ),
2139
+ configRow(
2140
+ "config:group-default",
2141
+ t("configGroupDefault"),
2142
+ t("configGroupDefaultHint"),
2143
+ React8.createElement(import_dsh_client_ui_primitives6.SegmentedControl, {
2144
+ id: "session-curator-group-default",
2145
+ value: settings.groupDefault,
2146
+ options: [
2147
+ { value: "expanded", label: t("groupExpanded") },
2148
+ { value: "collapsed", label: t("groupCollapsed") }
2149
+ ],
2150
+ label: t("configGroupDefault"),
2151
+ onChange: (next) => void updateSettings({ groupDefault: next })
2152
+ })
2153
+ ),
2154
+ configRow(
2155
+ "config:sidebar-archived",
2156
+ t("configSidebarArchived"),
2157
+ t("configSidebarArchivedHint"),
2158
+ React8.createElement(import_dsh_client_ui_primitives6.Switch, {
2159
+ checked: sidebarArchived !== "default",
2160
+ label: t("configSidebarArchived"),
2161
+ onChange: (next) => {
2162
+ writeSidebarArchived(next);
2163
+ setSidebarArchived(next ? "show" : "default");
2164
+ }
2165
+ })
2166
+ ),
1116
2167
  // 官方 store 只在创建时读一次 localStorage,所以改完必须刷新页面。
1117
- sidebarArchived === sidebarAtMount ? null : React5.createElement(
2168
+ sidebarArchived === sidebarAtMount ? null : React8.createElement(
1118
2169
  "div",
1119
2170
  { key: "config:sidebar-refresh", style: S.configFoot },
1120
- React5.createElement("span", { style: S.configHint }, t("configSidebarRefreshHint")),
1121
- React5.createElement(import_dsh_client_ui_primitives4.Button, {
1122
- size: "sm",
1123
- variant: "outline",
1124
- onClick: () => {
1125
- try {
1126
- globalThis.location.reload();
1127
- } catch {
2171
+ React8.createElement("span", { style: S.configHint }, t("configSidebarRefreshHint")),
2172
+ React8.createElement(
2173
+ import_dsh_client_ui_primitives6.Button,
2174
+ {
2175
+ size: "sm",
2176
+ variant: "outline",
2177
+ onClick: () => {
2178
+ try {
2179
+ globalThis.location.reload();
2180
+ } catch {
2181
+ }
1128
2182
  }
1129
- }
1130
- }, t("refreshPage"))
2183
+ },
2184
+ t("refreshPage")
2185
+ )
1131
2186
  ),
1132
- configRow("config:meta", t("configMeta"), t("configMetaHint"), React5.createElement(import_dsh_client_ui_primitives4.Switch, {
1133
- checked: settings.showMeta,
1134
- label: t("configMeta"),
1135
- onChange: (next) => void updateSettings({ showMeta: next })
1136
- })),
1137
- configRow("config:order", t("configOrder"), t("configOrderHint"), React5.createElement(import_dsh_client_ui_primitives4.SegmentedControl, {
1138
- id: "session-curator-order",
1139
- value: settings.order,
1140
- options: [
1141
- { value: "host", label: t("orderHost") },
1142
- { value: "title", label: t("orderTitle") },
1143
- { value: "bytes", label: t("orderBytes") }
1144
- ],
1145
- label: t("configOrder"),
1146
- onChange: (next) => void updateSettings({ order: next })
1147
- })),
1148
- configRow("config:auto", t("configAutoUpdate"), t("configAutoUpdateHint"), React5.createElement(import_dsh_client_ui_primitives4.Switch, {
1149
- checked: settings.autoUpdate,
1150
- label: t("configAutoUpdate"),
1151
- onChange: (next) => void updateSettings({ autoUpdate: next })
1152
- })),
1153
- React5.createElement(
2187
+ configRow(
2188
+ "config:meta",
2189
+ t("configMeta"),
2190
+ t("configMetaHint"),
2191
+ React8.createElement(import_dsh_client_ui_primitives6.Switch, {
2192
+ checked: settings.showMeta,
2193
+ label: t("configMeta"),
2194
+ onChange: (next) => void updateSettings({ showMeta: next })
2195
+ })
2196
+ ),
2197
+ // 排序:5 档,所以是下拉(`Dropdown` 见 components/dropdown.ts)。
2198
+ configRow(
2199
+ "config:order",
2200
+ t("configOrder"),
2201
+ t("configOrderHint"),
2202
+ React8.createElement(Dropdown, {
2203
+ id: "session-curator-order",
2204
+ value: settings.order,
2205
+ options: orderOptions,
2206
+ label: t("configOrder"),
2207
+ open: openDropdown === "order",
2208
+ onToggle: (next) => setOpenDropdown(next ? "order" : null),
2209
+ onChange: (next) => void updateSettings({ order: next })
2210
+ })
2211
+ ),
2212
+ // ── 回收站保留策略 ──────────────────────────────────────────────
2213
+ // 这条**故意不做成自动清理**:它是本插件唯一不可撤销的动作。这里只让用户选一个
2214
+ // 天数(档位或自定义)并看到预览,真正的删除必须再点一次、再过一道确认弹窗(`purge-old`)。
2215
+ configRow(
2216
+ "config:trash-days",
2217
+ t("configTrashDays"),
2218
+ t("configTrashDaysHint"),
2219
+ React8.createElement(
2220
+ "div",
2221
+ { style: S.retentionDays },
2222
+ React8.createElement(Dropdown, {
2223
+ id: "session-curator-trash-days",
2224
+ value: settings.trashDays,
2225
+ options: retentionOptions,
2226
+ label: t("configTrashDays"),
2227
+ // 填了自定义天数时锚点显示"自定义",否则显示档位 —— 让"现在到底按哪个算"一眼可见。
2228
+ display: customDaysValid && customDays.trim() !== "" ? t("retentionCustom") : void 0,
2229
+ open: openDropdown === "trash-days",
2230
+ onToggle: (next) => setOpenDropdown(next ? "trash-days" : null),
2231
+ onChange: (next) => void updateSettings({ trashDays: next, trashDaysCustom: "" })
2232
+ }),
2233
+ React8.createElement(import_dsh_client_ui_primitives6.Input, {
2234
+ value: customDays,
2235
+ placeholder: t("customDaysPlaceholder"),
2236
+ "aria-label": t("customDaysLabel"),
2237
+ "aria-invalid": !customDaysValid,
2238
+ title: text(t, "customDaysHint", { min: TRASH_DAYS_MIN, max: TRASH_DAYS_MAX }),
2239
+ style: customDaysValid ? S.customDaysInput : { ...S.customDaysInput, ...S.customDaysInvalid },
2240
+ onChange: (event) => void updateSettings({ trashDaysCustom: event.target.value })
2241
+ })
2242
+ )
2243
+ ),
2244
+ retentionFoot,
2245
+ configRow(
2246
+ "config:auto",
2247
+ t("configAutoUpdate"),
2248
+ t("configAutoUpdateHint"),
2249
+ React8.createElement(import_dsh_client_ui_primitives6.Switch, {
2250
+ checked: settings.autoUpdate,
2251
+ label: t("configAutoUpdate"),
2252
+ onChange: (next) => void updateSettings({ autoUpdate: next })
2253
+ })
2254
+ ),
2255
+ React8.createElement(
1154
2256
  "div",
1155
2257
  { key: "config:reset", style: { ...S.configFoot, justifyContent: "flex-end" } },
1156
- React5.createElement(import_dsh_client_ui_primitives4.Button, {
1157
- size: "sm",
1158
- variant: "outline",
1159
- onClick: () => {
1160
- updateSettings({ ...DEFAULT_SETTINGS });
1161
- groupingStore.set(DEFAULT_SETTINGS.grouping);
1162
- }
1163
- }, t("configReset"))
2258
+ React8.createElement(
2259
+ import_dsh_client_ui_primitives6.Button,
2260
+ {
2261
+ size: "sm",
2262
+ variant: "outline",
2263
+ onClick: () => {
2264
+ updateSettings({ ...DEFAULT_SETTINGS });
2265
+ groupingStore.set(DEFAULT_SETTINGS.grouping);
2266
+ }
2267
+ },
2268
+ t("configReset")
2269
+ )
1164
2270
  )
1165
2271
  ),
1166
- React5.createElement(
2272
+ React8.createElement(
1167
2273
  "div",
1168
2274
  { style: S.card },
1169
- React5.createElement(
2275
+ React8.createElement(
1170
2276
  "div",
1171
2277
  { style: { ...S.configRow, borderTop: "none" } },
1172
- React5.createElement(
2278
+ React8.createElement(
1173
2279
  "div",
1174
2280
  { style: S.configMain },
1175
- React5.createElement("span", { style: S.configLabel }, t("aboutTitle")),
1176
- React5.createElement("span", { style: S.configHint }, "dsh-session-curator")
2281
+ React8.createElement("span", { style: S.configLabel }, t("aboutTitle")),
2282
+ React8.createElement("span", { style: S.configHint }, "dsh-session-curator")
1177
2283
  ),
1178
- React5.createElement(
2284
+ React8.createElement(
1179
2285
  "div",
1180
2286
  { style: S.configControl },
1181
2287
  // 版本号:浅色小字。
1182
- React5.createElement("span", { style: S.version }, `${t("versionLabel")} ${version === null ? "—" : `v${version}`}`),
1183
- React5.createElement(import_dsh_client_ui_primitives4.Button, {
1184
- size: "sm",
1185
- variant: "outline",
1186
- disabled: update.state === "checking",
1187
- onClick: () => void checkUpdate(true)
1188
- }, t("updateCheckNow"))
2288
+ React8.createElement("span", { style: S.version }, `${t("versionLabel")} ${version === null ? "—" : `v${version}`}`),
2289
+ React8.createElement(
2290
+ import_dsh_client_ui_primitives6.Button,
2291
+ {
2292
+ size: "sm",
2293
+ variant: "outline",
2294
+ disabled: update.state === "checking",
2295
+ onClick: () => void checkUpdate(true)
2296
+ },
2297
+ t("updateCheckNow")
2298
+ )
1189
2299
  )
1190
2300
  ),
1191
- updateLine === null ? null : React5.createElement("div", { key: "config:update", style: S.configFoot }, updateLine),
1192
- outdated && update.value !== null ? React5.createElement(
2301
+ updateLine === null ? null : React8.createElement("div", { key: "config:update", style: S.configFoot }, updateLine),
2302
+ outdated && update.value !== null ? React8.createElement(
1193
2303
  "div",
1194
2304
  { key: "config:upgrade", style: S.updateBox },
1195
- React5.createElement("span", { style: S.updateTitle }, text(t, "updateAvailable", { latest: update.value.latest ?? "?", current: version === null ? "?" : version })),
1196
- React5.createElement(
2305
+ React8.createElement(
2306
+ "span",
2307
+ { style: S.updateTitle },
2308
+ text(t, "updateAvailable", { latest: update.value.latest ?? "?", current: version === null ? "?" : version })
2309
+ ),
2310
+ React8.createElement(
1197
2311
  "span",
1198
2312
  { style: S.updateSteps },
1199
- React5.createElement("span", null, t("updateStepsTitle")),
1200
- React5.createElement("span", null, t("updateStepOne")),
1201
- React5.createElement("code", { style: S.code }, "dsh plugin --profile desktop add dsh-session-curator@latest"),
1202
- React5.createElement("span", null, t("updateStepTwo"))
2313
+ React8.createElement("span", null, t("updateStepsTitle")),
2314
+ React8.createElement("span", null, t("updateStepOne")),
2315
+ React8.createElement("code", { style: S.code }, "dsh plugin --profile desktop add dsh-session-curator@latest"),
2316
+ React8.createElement("span", null, t("updateStepTwo"))
1203
2317
  ),
1204
- React5.createElement("a", {
1205
- style: S.link,
1206
- href: "https://www.npmjs.com/package/dsh-session-curator",
1207
- target: "_blank",
1208
- rel: "noreferrer"
1209
- }, t("updateOpen"))
2318
+ React8.createElement(
2319
+ "a",
2320
+ {
2321
+ style: S.link,
2322
+ href: "https://www.npmjs.com/package/dsh-session-curator",
2323
+ target: "_blank",
2324
+ rel: "noreferrer"
2325
+ },
2326
+ t("updateOpen")
2327
+ )
1210
2328
  ) : null
1211
2329
  )
1212
2330
  );
1213
- return React5.createElement(
2331
+ const usagePage = React8.createElement(
1214
2332
  "div",
1215
2333
  { style: S.column },
1216
- React5.createElement(
2334
+ React8.createElement(
2335
+ "div",
2336
+ { style: S.rowLine },
2337
+ React8.createElement("div", { style: S.filterSpacer }),
2338
+ React8.createElement(
2339
+ import_dsh_client_ui_primitives6.Button,
2340
+ {
2341
+ size: "sm",
2342
+ variant: "outline",
2343
+ disabled: usageBusy,
2344
+ onClick: () => void refreshUsage()
2345
+ },
2346
+ t("refresh")
2347
+ )
2348
+ ),
2349
+ React8.createElement(UsagePanel, {
2350
+ t,
2351
+ usage,
2352
+ loading: usageBusy,
2353
+ error: usageError,
2354
+ busy,
2355
+ // 榜单上的动作走与列表同一个端点(`run()` 会顺手刷新快照),但**完事之后还要
2356
+ // 重拉一次 `usage`**:`run()` 只换快照,`usage` 不在快照里 —— 不重拉的话
2357
+ // 刚点了「取消归档」的那一行仍然顶着「已归档」胶囊,看起来像没生效。
2358
+ onArchive: (sessionId) => runUsageAction("archive", sessionId),
2359
+ onUnarchive: (sessionId) => runUsageAction("unarchive", sessionId)
2360
+ })
2361
+ );
2362
+ return React8.createElement(
2363
+ "div",
2364
+ { style: S.column },
2365
+ React8.createElement(
1217
2366
  "div",
1218
2367
  { style: S.header },
1219
- React5.createElement(
2368
+ React8.createElement(
1220
2369
  "div",
1221
2370
  { style: S.tabBar },
1222
- tabs.map((entry) => React5.createElement(import_dsh_client_ui_primitives4.Button, {
1223
- key: entry.id,
1224
- size: "sm",
1225
- variant: tab === entry.id ? "primary" : "ghost",
1226
- onClick: () => setTab(entry.id)
1227
- }, `${entry.label}${entry.count > 0 ? ` (${entry.count})` : ""}`))
2371
+ tabs.map(
2372
+ (entry) => React8.createElement(
2373
+ import_dsh_client_ui_primitives6.Button,
2374
+ {
2375
+ key: entry.id,
2376
+ size: "sm",
2377
+ variant: tab === entry.id ? "primary" : "ghost",
2378
+ onClick: () => setTab(entry.id)
2379
+ },
2380
+ `${entry.label}${entry.count > 0 ? ` (${entry.count})` : ""}`
2381
+ )
2382
+ )
1228
2383
  ),
1229
- // 分组与刷新推到右边;配置页上没有它们。
1230
- isConfig ? null : React5.createElement(
2384
+ // 分组与刷新推到右边;只有三个**数据**页签上才有它们(配置页与磁盘占用页都没有
2385
+ // 可筛可选的列表,磁盘占用页自带一个只重拉 usage 的刷新按钮)。
2386
+ isDataTab ? React8.createElement(
1231
2387
  "div",
1232
2388
  { style: S.headerActions },
1233
- React5.createElement(import_dsh_client_ui_primitives4.Button, {
1234
- size: "sm",
1235
- variant: "outline",
1236
- title: t("groupHint"),
1237
- onClick: () => {
1238
- const next = !grouping;
1239
- groupingStore.set(next);
1240
- updateSettings({ grouping: next });
1241
- }
1242
- }, grouping ? t("groupingOn") : t("groupingOff")),
1243
- React5.createElement(import_dsh_client_ui_primitives4.Button, { size: "sm", variant: "outline", disabled: loading || busy, onClick: () => void refresh() }, t("refresh"))
1244
- )
2389
+ React8.createElement(
2390
+ import_dsh_client_ui_primitives6.Button,
2391
+ {
2392
+ size: "sm",
2393
+ variant: "outline",
2394
+ title: t("groupHint"),
2395
+ onClick: () => {
2396
+ const next = !grouping;
2397
+ groupingStore.set(next);
2398
+ updateSettings({ grouping: next });
2399
+ }
2400
+ },
2401
+ grouping ? t("groupingOn") : t("groupingOff")
2402
+ ),
2403
+ React8.createElement(
2404
+ import_dsh_client_ui_primitives6.Button,
2405
+ { size: "sm", variant: "outline", disabled: loading || busy, onClick: () => void refresh() },
2406
+ t("refresh")
2407
+ )
2408
+ ) : null
1245
2409
  ),
1246
- error ? React5.createElement("div", { style: S.error }, error) : null,
1247
- isConfig ? null : React5.createElement(
2410
+ error ? React8.createElement("div", { style: S.error }, error) : null,
2411
+ // 过滤工具条:只有数据页签上有(配置页没有列表可筛,磁盘占用页也不是列表)。
2412
+ isDataTab ? filterBar : null,
2413
+ isDataTab ? React8.createElement(
1248
2414
  "div",
1249
2415
  { style: S.card },
1250
- loading && !snapshot ? React5.createElement("div", { style: S.empty }, t("loading")) : rows.length === 0 ? React5.createElement("div", { style: S.empty }, tab === "archived" ? t("emptyArchived") : tab === "pinned" ? t("emptyPinned") : t("emptyTrash")) : React5.createElement("div", { style: S.list }, nodes)
1251
- ),
1252
- isConfig ? null : React5.createElement(
2416
+ loading && !snapshot ? React8.createElement("div", { style: S.empty }, t("loading")) : filtered.length === 0 ? React8.createElement(
2417
+ "div",
2418
+ { style: S.empty },
2419
+ filtering ? t("emptyFiltered") : tab === "archived" ? t("emptyArchived") : tab === "pinned" ? t("emptyPinned") : t("emptyTrash")
2420
+ ) : React8.createElement("div", { style: S.list }, nodes)
2421
+ ) : null,
2422
+ isDataTab ? React8.createElement(
1253
2423
  "div",
1254
2424
  { style: S.rowLine },
1255
- React5.createElement(import_dsh_client_ui_primitives4.Button, {
1256
- size: "sm",
1257
- variant: "ghost",
1258
- disabled: busy || rows.length === 0,
1259
- onClick: () => setSelected(selected.length === rows.length ? [] : rows.map((row) => row.sessionId))
1260
- }, selected.length === rows.length && rows.length > 0 ? t("selectNone") : t("selectAll")),
1261
- batch.map((action) => React5.createElement(import_dsh_client_ui_primitives4.Button, {
1262
- key: action.label,
1263
- size: "sm",
1264
- variant: action.variant || "ghost",
1265
- disabled: busy,
1266
- onClick: action.run
1267
- }, action.label)),
1268
- tab === "trash" && trashed.length > 0 ? React5.createElement(import_dsh_client_ui_primitives4.Button, {
1269
- size: "sm",
1270
- variant: "outline",
1271
- disabled: busy,
1272
- onClick: () => requestPurge(trashed.map((row) => row.sessionId), "")
1273
- }, t("emptyTrashAll")) : null,
1274
- busy ? React5.createElement("span", { style: S.footer }, t("working")) : null
1275
- ),
2425
+ React8.createElement(
2426
+ import_dsh_client_ui_primitives6.Button,
2427
+ {
2428
+ size: "sm",
2429
+ variant: "ghost",
2430
+ disabled: busy || rows.length === 0,
2431
+ onClick: () => setSelected(selected.length === rows.length ? [] : rows.map((row) => row.sessionId))
2432
+ },
2433
+ selected.length === rows.length && rows.length > 0 ? t("selectNone") : t("selectAll")
2434
+ ),
2435
+ batch.map(
2436
+ (action) => React8.createElement(
2437
+ import_dsh_client_ui_primitives6.Button,
2438
+ {
2439
+ key: action.label,
2440
+ size: "sm",
2441
+ variant: action.variant || "ghost",
2442
+ disabled: busy || action.disabled === true,
2443
+ onClick: action.run
2444
+ },
2445
+ action.label
2446
+ )
2447
+ ),
2448
+ tab === "trash" && trashed.length > 0 ? React8.createElement(
2449
+ import_dsh_client_ui_primitives6.Button,
2450
+ {
2451
+ size: "sm",
2452
+ variant: "outline",
2453
+ disabled: busy,
2454
+ onClick: () => requestPurge(
2455
+ trashed.map((row) => row.sessionId),
2456
+ ""
2457
+ )
2458
+ },
2459
+ t("emptyTrashAll")
2460
+ ) : null,
2461
+ busy ? React8.createElement("span", { style: S.footer }, t("working")) : null
2462
+ ) : null,
2463
+ isUsage ? usagePage : null,
1276
2464
  isConfig ? configPage : null,
1277
- isConfig ? null : React5.createElement(
2465
+ isDataTab ? React8.createElement(
1278
2466
  "div",
1279
2467
  { style: S.footer },
1280
- React5.createElement("span", null, text(t, "total", { n: rows.length, size: formatBytes(totalBytes) })),
1281
- grouping && groups.length > 0 ? React5.createElement("span", null, text(t, "workspaces", { n: groups.length })) : null,
1282
- selected.length > 0 ? React5.createElement("span", null, text(t, "selected", { n: selected.length })) : null,
1283
- snapshot ? React5.createElement("span", { style: S.mono, title: snapshot.trashDir }, snapshot.trashDir) : null
1284
- )
2468
+ React8.createElement("span", null, text(t, "total", { n: rows.length, size: formatBytes(totalBytes) })),
2469
+ // 过滤只改显示:合计那一行报的仍是整个页签的条数,这里再补一句"筛出来几条"。
2470
+ filtering ? React8.createElement("span", null, text(t, "filtered", { n: filtered.length, total: rows.length })) : null,
2471
+ grouping && groups.length > 0 ? React8.createElement("span", null, text(t, "workspaces", { n: groups.length })) : null,
2472
+ selected.length > 0 ? React8.createElement("span", null, text(t, "selected", { n: selected.length })) : null,
2473
+ snapshot ? React8.createElement("span", { style: S.mono, title: snapshot.trashDir }, snapshot.trashDir) : null
2474
+ ) : null
2475
+ );
2476
+ }
2477
+
2478
+ // src/client/components/toast-host.ts
2479
+ var React9 = __toESM(require("react"), 1);
2480
+ var import_dsh_client_ui_primitives7 = require("@deepseek-ai/dsh-client-ui-primitives");
2481
+ var HOLD_MS = 5e3;
2482
+ function ToastHost(props) {
2483
+ const t = props.t;
2484
+ const notice = useStore(toastStore);
2485
+ if (notice === null) return null;
2486
+ if (notice.error !== null) {
2487
+ return React9.createElement(
2488
+ import_dsh_client_ui_primitives7.Toast,
2489
+ {
2490
+ text: text(t, "toastUndoFailed", { reason: notice.error }),
2491
+ icon: React9.createElement(import_dsh_client_ui_primitives7.IconWarningOutlineRegular, {}),
2492
+ onDone: dismissToast
2493
+ },
2494
+ `session-curator-toast-${notice.seq}`
2495
+ );
2496
+ }
2497
+ if (notice.ids.length === 0) {
2498
+ return React9.createElement(
2499
+ import_dsh_client_ui_primitives7.Toast,
2500
+ {
2501
+ text: notice.title,
2502
+ holdMs: HOLD_MS,
2503
+ onDone: dismissToast
2504
+ },
2505
+ `session-curator-toast-${notice.seq}`
2506
+ );
2507
+ }
2508
+ const many = notice.ids.length > 1;
2509
+ return React9.createElement(
2510
+ import_dsh_client_ui_primitives7.Toast,
2511
+ {
2512
+ text: many ? text(t, "toastTrashedMany", { n: notice.ids.length }) : text(t, "toastTrashed", { title: notice.title }),
2513
+ tone: "success",
2514
+ holdMs: HOLD_MS,
2515
+ actions: [{ label: t("toastUndo"), onClick: () => void undoTrash() }],
2516
+ onDone: dismissToast
2517
+ },
2518
+ `session-curator-toast-${notice.seq}`
1285
2519
  );
1286
2520
  }
1287
2521
 
2522
+ // src/client/components/trash-row-button.ts
2523
+ var React10 = __toESM(require("react"), 1);
2524
+ var import_dsh_client_ui_primitives8 = require("@deepseek-ai/dsh-client-ui-primitives");
2525
+ function TrashRowButton(props) {
2526
+ const [hovered, setHovered] = React10.useState(false);
2527
+ const title = props.displayTitle || props.sessionId;
2528
+ const label = props.t("moveToTrash");
2529
+ return React10.createElement(import_dsh_client_ui_primitives8.Tooltip, {
2530
+ label,
2531
+ side: "bottom",
2532
+ align: "end",
2533
+ delayMs: 500,
2534
+ children: React10.createElement(
2535
+ "button",
2536
+ {
2537
+ type: "button",
2538
+ style: hovered ? S.rowIconButtonHover : S.rowIconButton,
2539
+ "aria-label": `${title} — ${label}`,
2540
+ onPointerEnter: () => setHovered(true),
2541
+ onPointerLeave: () => setHovered(false),
2542
+ onClick: () => requestTrash([props.sessionId], title)
2543
+ },
2544
+ React10.createElement(import_dsh_client_ui_primitives8.IconTrashOutlineRegular, { size: 14 })
2545
+ )
2546
+ });
2547
+ }
2548
+
1288
2549
  // src/client/index.ts
1289
2550
  var MENU_ID = "session-curator.delete";
1290
2551
  var OVERLAY_ID = "session-curator.dialog";
2552
+ var TOAST_ID = "session-curator.toast";
2553
+ var HOVER_ID = "session-curator.hover";
2554
+ var ROW_ACTION_ID = "session-curator.trash";
1291
2555
  var SECTION_ID = "session-curator";
1292
2556
  var NAV_SECTION_ID = "archived-sessions";
1293
2557
  function sectionIdFor(ctx) {
@@ -1311,36 +2575,89 @@ function apply(ctx) {
1311
2575
  if (ctx.logger) ctx.logger.warn(`[session-curator] slot ${slot} 注册失败(${label})`);
1312
2576
  }
1313
2577
  };
1314
- contribute("sidebar.workspaces.session.menu.item", {
1315
- name: "sidebar.workspaces.session.menu.item",
1316
- id: MENU_ID,
1317
- order: 500,
1318
- label: () => t("menuDelete"),
1319
- locale: NS
1320
- }, (props) => {
1321
- const [, setMenuOpen] = props.useMenuOpenState();
1322
- return React6.createElement(import_dsh_client_ui_primitives5.MenuItemButton, {
1323
- danger: true,
1324
- separatorBefore: true,
1325
- onSelect: () => {
1326
- setMenuOpen(false);
1327
- requestTrash([props.sessionId], props.displayTitle || props.sessionId);
1328
- }
1329
- }, t("menuDelete"));
1330
- }, "menu item");
1331
- contribute("shell.overlay", {
1332
- name: "shell.overlay",
1333
- id: OVERLAY_ID,
1334
- order: 60,
1335
- locale: NS
1336
- }, DialogHost, "dialog host");
1337
- contribute("settings.section", {
1338
- name: "settings.section",
1339
- id: sectionIdFor(ctx),
1340
- order: 45,
1341
- label: () => t("sectionLabel"),
1342
- locale: NS
1343
- }, SessionManagerPanel, "settings section");
2578
+ contribute(
2579
+ "sidebar.workspaces.session.menu.item",
2580
+ {
2581
+ name: "sidebar.workspaces.session.menu.item",
2582
+ id: MENU_ID,
2583
+ order: 500,
2584
+ label: () => t("menuDelete"),
2585
+ locale: NS
2586
+ },
2587
+ (props) => {
2588
+ const [, setMenuOpen] = props.useMenuOpenState();
2589
+ return React11.createElement(
2590
+ import_dsh_client_ui_primitives9.MenuItemButton,
2591
+ {
2592
+ danger: true,
2593
+ separatorBefore: true,
2594
+ onSelect: () => {
2595
+ setMenuOpen(false);
2596
+ requestTrash([props.sessionId], props.displayTitle || props.sessionId);
2597
+ }
2598
+ },
2599
+ t("menuDelete")
2600
+ );
2601
+ },
2602
+ "menu item"
2603
+ );
2604
+ contribute(
2605
+ "shell.overlay",
2606
+ {
2607
+ name: "shell.overlay",
2608
+ id: OVERLAY_ID,
2609
+ order: 60,
2610
+ locale: NS
2611
+ },
2612
+ DialogHost,
2613
+ "dialog host"
2614
+ );
2615
+ contribute(
2616
+ "settings.section",
2617
+ {
2618
+ name: "settings.section",
2619
+ id: sectionIdFor(ctx),
2620
+ order: 45,
2621
+ label: () => t("sectionLabel"),
2622
+ locale: NS
2623
+ },
2624
+ SessionManagerPanel,
2625
+ "settings section"
2626
+ );
2627
+ contribute(
2628
+ "sidebar.session.row.hover",
2629
+ {
2630
+ name: "sidebar.session.row.hover",
2631
+ id: HOVER_ID,
2632
+ // 官方 ui-schedule 的任务列表在这个槽位是 order 10;我们排在它后面。
2633
+ order: 50,
2634
+ locale: NS
2635
+ },
2636
+ SessionHover,
2637
+ "hover card"
2638
+ );
2639
+ contribute(
2640
+ "sidebar.workspaces.session.row.action",
2641
+ {
2642
+ name: "sidebar.workspaces.session.row.action",
2643
+ id: ROW_ACTION_ID,
2644
+ order: 500,
2645
+ locale: NS
2646
+ },
2647
+ TrashRowButton,
2648
+ "row action"
2649
+ );
2650
+ contribute(
2651
+ "shell.overlay",
2652
+ {
2653
+ name: "shell.overlay",
2654
+ id: TOAST_ID,
2655
+ order: 70,
2656
+ locale: NS
2657
+ },
2658
+ ToastHost,
2659
+ "toast host"
2660
+ );
1344
2661
  }
1345
2662
  })(module, module.exports, require);
1346
2663
  return module.exports;