dsh-session-curator 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -37,21 +37,37 @@ 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
+ UsagePanel: () => UsagePanel,
43
45
  apply: () => apply,
46
+ cachedSnapshot: () => cachedSnapshot,
44
47
  collapseKey: () => collapseKey,
48
+ describeSession: () => describeSession,
49
+ dialogStore: () => dialogStore,
50
+ filterRows: () => filterRows,
51
+ forgetDetails: () => forgetDetails,
52
+ forgetSnapshot: () => forgetSnapshot,
45
53
  groupKeyOf: () => groupKeyOf,
46
54
  groupRows: () => groupRows,
47
55
  inject: () => inject,
56
+ isFilterActive: () => isFilterActive,
48
57
  isGroupCollapsed: () => isGroupCollapsed,
58
+ matchesFilter: () => matchesFilter,
59
+ matchesSearch: () => matchesSearch,
60
+ mutationStore: () => mutationStore,
49
61
  sectionIdFor: () => sectionIdFor,
50
- toggledCollapse: () => toggledCollapse
62
+ showNotice: () => showNotice,
63
+ sortRows: () => sortRows,
64
+ toastStore: () => toastStore,
65
+ toggledCollapse: () => toggledCollapse,
66
+ workspaceSummaries: () => workspaceSummaries
51
67
  });
52
68
  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");
69
+ var React11 = __toESM(require("react"), 1);
70
+ var import_dsh_client_ui_primitives9 = require("@deepseek-ai/dsh-client-ui-primitives");
55
71
 
56
72
  // src/client/components/dialog-host.ts
57
73
  var React2 = __toESM(require("react"), 1);
@@ -168,49 +184,38 @@ function useStore(store) {
168
184
  // src/client/dialog.ts
169
185
  var dialogStore = createStore(null);
170
186
  var mutationStore = createStore(0);
171
- function requestTrash(ids, title) {
172
- dialogStore.set({ kind: "trash", ids, title });
173
- }
174
- function requestPurge(ids, title) {
175
- dialogStore.set({ kind: "purge", ids, title });
187
+ function requestDelete(ids, title) {
188
+ dialogStore.set({ kind: "delete", ids, title });
176
189
  }
177
190
 
178
191
  // src/client/i18n.ts
179
192
  var NS = "session-curator";
180
193
  var zh = {
181
194
  sectionLabel: "会话管理",
182
- menuDelete: "删除会话",
195
+ deleteSession: "删除会话",
183
196
  titleArchived: "已归档",
184
197
  titlePinned: "已置顶",
185
- titleTrash: "回收站",
186
198
  refresh: "刷新",
187
199
  loading: "读取中…",
188
200
  retry: "重试",
189
201
  emptyArchived: "还没有归档的会话。在会话行的「⋯」菜单里归档后会出现在这里。",
190
202
  emptyPinned: "没有置顶的会话。",
191
- emptyTrash: "回收站是空的。",
192
203
  unarchive: "取消归档",
193
204
  pin: "置顶",
194
205
  unpin: "取消置顶",
195
- restore: "恢复",
196
- purge: "永久删除",
197
- moveToTrash: "移入回收站",
198
206
  rowActions: "更多操作",
199
207
  selectAll: "全选",
200
208
  selectNone: "取消选择",
201
209
  selected: "已选 {n} 项",
202
- emptyTrashAll: "清空回收站",
203
210
  total: "合计 {n} 个会话 · {size}",
204
211
  working: "处理中…",
205
212
  sessionRunning: "会话正在运行,先停止它再删除。",
206
- confirmTrashTitle: "删除会话",
207
- confirmTrashBody: "「{title}」会被移到回收站,之后可以在「设置 → 会话管理」里恢复。",
208
- confirmTrashAction: "移到回收站",
209
- confirmPurgeTitle: "永久删除",
210
- confirmPurgeBody: "「{title}」将从回收站永久删除,日志文件会被彻底移除,无法恢复。",
211
- confirmPurgeManyBody: "选中的 {n} 个会话将从回收站永久删除,无法恢复。",
212
- confirmPurgeAction: "永久删除",
213
- ackPurge: "我明白这无法撤销",
213
+ // 物理删除的确认弹窗(唯一一种弹窗):正文必须写清"不可恢复"并指路归档。
214
+ confirmDeleteTitle: "永久删除",
215
+ confirmDeleteBody: "「{title}」将被永久删除:日志文件会从磁盘上彻底移除,无法恢复。如果想以后还能找回,请改用「归档」。",
216
+ confirmDeleteManyBody: "选中的 {n} 个会话将被永久删除:日志文件会从磁盘上彻底移除,无法恢复。如果想以后还能找回,请改用「归档」。",
217
+ confirmDeleteAction: "永久删除",
218
+ ackDelete: "我明白这无法撤销",
214
219
  cancel: "取消",
215
220
  close: "关闭",
216
221
  noProject: "(未知目录)",
@@ -226,6 +231,35 @@ var zh = {
226
231
  clearShort: "取消",
227
232
  collapse: "折叠",
228
233
  expand: "展开",
234
+ // 跨页签的"整个工作区一起处理"(分组标题右侧的「⋯」)。
235
+ workspaceActions: "整个工作区",
236
+ workspaceMenu: "工作区操作",
237
+ archiveWorkspace: "整个工作区归档",
238
+ deleteWorkspace: "整个工作区删除",
239
+ workspaceRunningHint: "这个工作区里有会话正在运行,危险动作已禁用",
240
+ // 运行中徽标(宿主用官方的 workspace/session-activity 判定)。
241
+ running: "运行中",
242
+ runningHint: "会话正在运行,先停止它再操作",
243
+ // 导出 / 定位。
244
+ copyPath: "复制路径",
245
+ revealInFolder: "在资源管理器中显示",
246
+ pathCopied: "已复制路径:{path}",
247
+ pathCopyFailed: "复制失败,路径是:{path}",
248
+ revealFailed: "没能打开文件管理器({reason}),路径是:{path}",
249
+ // 磁盘占用视图。
250
+ titleUsage: "磁盘占用",
251
+ usageTitle: "磁盘占用",
252
+ usageTotal: "共 {n} 个会话 · {size}",
253
+ usageByWorkspace: "按工作区",
254
+ usageLargest: "最占地方的会话",
255
+ usageOldest: "最久没打开的会话",
256
+ usageLoading: "正在统计…",
257
+ usageArchived: "已归档",
258
+ usagePinned: "已置顶",
259
+ usageSuggestArchive: "建议归档",
260
+ usageArchive: "归档",
261
+ usageUnarchive: "取消归档",
262
+ usageNoData: "没有可统计的会话。",
229
263
  titleConfig: "配置",
230
264
  configTitle: "基础设置",
231
265
  configTab: "默认页签",
@@ -243,10 +277,20 @@ var zh = {
243
277
  configMeta: "行内显示体积与文件数",
244
278
  configMetaHint: "关掉后每行只留标题(平铺时仍会显示文件夹)",
245
279
  configOrder: "排序",
246
- configOrderHint: "原顺序就是宿主给的顺序(回收站里是最近删除在前)",
280
+ configOrderHint: "原顺序就是宿主给的顺序",
247
281
  orderHost: "原顺序",
248
282
  orderTitle: "标题",
249
283
  orderBytes: "体积",
284
+ orderCreatedAt: "创建时间",
285
+ // 搜索与过滤(只影响显示;批量操作与计数仍按整个页签算)。
286
+ searchLabel: "搜索会话",
287
+ searchPlaceholder: "搜索标题或目录…",
288
+ searchClear: "清除搜索",
289
+ timeFilterLabel: "时间",
290
+ timeAll: "全部",
291
+ timeRecent: "最近 7 天",
292
+ filtered: "已过滤出 {n} 个会话(共 {total} 个)",
293
+ emptyFiltered: "没有符合搜索条件的会话。",
250
294
  configAutoUpdate: "自动检查更新",
251
295
  configAutoUpdateHint: "打开这个页面时问一次 npm registry(10 分钟内有缓存)",
252
296
  configReset: "恢复默认",
@@ -263,42 +307,39 @@ var zh = {
263
307
  updateStepsTitle: "升级步骤",
264
308
  updateStepOne: "在终端里升级包(按你自己的 profile 名替换 desktop):",
265
309
  updateStepTwo: "重启 DSH 让宿主半区生效;只改了客户端半区的话刷新页面就够。",
266
- updateOpen: "打开 npm 页面"
310
+ updateOpen: "打开 npm 页面",
311
+ // 会话行 hover 卡片(`sidebar.session.row.hover`)。
312
+ hoverStats: "{size} · {n} 个文件",
313
+ hoverCwd: "原目录",
314
+ hoverArchived: "已归档",
315
+ hoverPinned: "已置顶"
267
316
  };
268
317
  var en = {
269
318
  sectionLabel: "Session manager",
270
- menuDelete: "Delete session",
319
+ deleteSession: "Delete session",
271
320
  titleArchived: "Archived",
272
321
  titlePinned: "Pinned",
273
- titleTrash: "Trash",
274
322
  refresh: "Refresh",
275
323
  loading: "Loading…",
276
324
  retry: "Retry",
277
325
  emptyArchived: "No archived sessions yet. Archive one from its “⋯” menu and it shows up here.",
278
326
  emptyPinned: "No pinned sessions.",
279
- emptyTrash: "The trash is empty.",
280
327
  unarchive: "Unarchive",
281
328
  pin: "Pin",
282
329
  unpin: "Unpin",
283
- restore: "Restore",
284
- purge: "Delete permanently",
285
- moveToTrash: "Move to trash",
286
330
  rowActions: "More actions",
287
331
  selectAll: "Select all",
288
332
  selectNone: "Clear selection",
289
333
  selected: "{n} selected",
290
- emptyTrashAll: "Empty trash",
291
334
  total: "{n} sessions · {size}",
292
335
  working: "Working…",
293
336
  sessionRunning: "This session is running; stop it before deleting.",
294
- confirmTrashTitle: "Delete session",
295
- confirmTrashBody: "“{title}” moves to the trash. You can restore it later under Settings → Session manager.",
296
- confirmTrashAction: "Move to trash",
297
- confirmPurgeTitle: "Delete permanently",
298
- confirmPurgeBody: "“{title}” is removed from the trash for good; its log files are gone and cannot be recovered.",
299
- confirmPurgeManyBody: "{n} selected sessions are removed from the trash for good and cannot be recovered.",
300
- confirmPurgeAction: "Delete permanently",
301
- ackPurge: "I understand this cannot be undone",
337
+ // The one confirmation dialog: it must spell out "permanent" and point at archiving instead.
338
+ confirmDeleteTitle: "Delete permanently",
339
+ confirmDeleteBody: "“{title}” is deleted permanently: its log files are removed from disk for good and cannot be recovered. If you want to be able to bring it back later, use Archive instead.",
340
+ confirmDeleteManyBody: "{n} selected sessions are deleted permanently: their log files are removed from disk for good and cannot be recovered. If you want to be able to bring them back later, use Archive instead.",
341
+ confirmDeleteAction: "Delete permanently",
342
+ ackDelete: "I understand this cannot be undone",
302
343
  cancel: "Cancel",
303
344
  close: "Close",
304
345
  noProject: "(unknown directory)",
@@ -314,6 +355,35 @@ var en = {
314
355
  clearShort: "Clear",
315
356
  collapse: "Collapse",
316
357
  expand: "Expand",
358
+ // Cross-tab "handle the whole workspace" (the ⋯ on a folder header).
359
+ workspaceActions: "Whole workspace",
360
+ workspaceMenu: "Workspace actions",
361
+ archiveWorkspace: "Archive this whole workspace",
362
+ deleteWorkspace: "Delete this whole workspace",
363
+ workspaceRunningHint: "A session in this workspace is running, so the destructive actions are off",
364
+ // Running badge (the host asks the official workspace/session-activity hook).
365
+ running: "Running",
366
+ runningHint: "This session is running; stop it first",
367
+ // Export / locate.
368
+ copyPath: "Copy path",
369
+ revealInFolder: "Show in file explorer",
370
+ pathCopied: "Path copied: {path}",
371
+ pathCopyFailed: "Copy failed — the path is: {path}",
372
+ revealFailed: "Could not open the file explorer ({reason}); the path is: {path}",
373
+ // Disk usage view.
374
+ titleUsage: "Disk usage",
375
+ usageTitle: "Disk usage",
376
+ usageTotal: "{n} sessions · {size}",
377
+ usageByWorkspace: "By workspace",
378
+ usageLargest: "Largest sessions",
379
+ usageOldest: "Least recently opened",
380
+ usageLoading: "Measuring…",
381
+ usageArchived: "Archived",
382
+ usagePinned: "Pinned",
383
+ usageSuggestArchive: "Archive suggested",
384
+ usageArchive: "Archive",
385
+ usageUnarchive: "Unarchive",
386
+ usageNoData: "No sessions to measure.",
317
387
  titleConfig: "Settings",
318
388
  configTitle: "Basics",
319
389
  configTab: "Default tab",
@@ -331,10 +401,20 @@ var en = {
331
401
  configMeta: "Show size and file count per row",
332
402
  configMetaHint: "Off leaves only the title (the folder still shows in the flat list)",
333
403
  configOrder: "Order",
334
- configOrderHint: "Host order is what the host sent (the trash is most-recently-deleted first)",
404
+ configOrderHint: "Host order is what the host sent",
335
405
  orderHost: "Host",
336
406
  orderTitle: "Title",
337
407
  orderBytes: "Size",
408
+ orderCreatedAt: "Created",
409
+ // Search and filtering (display only; batch actions and counts still cover the whole tab).
410
+ searchLabel: "Search sessions",
411
+ searchPlaceholder: "Search title or folder…",
412
+ searchClear: "Clear search",
413
+ timeFilterLabel: "Time",
414
+ timeAll: "All",
415
+ timeRecent: "Last 7 days",
416
+ filtered: "{n} of {total} sessions match",
417
+ emptyFiltered: "No session matches the search.",
338
418
  configAutoUpdate: "Check for updates automatically",
339
419
  configAutoUpdateHint: "Asks the npm registry when this page opens (cached for 10 minutes)",
340
420
  configReset: "Reset to defaults",
@@ -351,7 +431,12 @@ var en = {
351
431
  updateStepsTitle: "How to upgrade",
352
432
  updateStepOne: "Upgrade the package in a terminal (replace desktop with your profile name):",
353
433
  updateStepTwo: "Restart DSH so the host half reloads; a client-half change only needs a page refresh.",
354
- updateOpen: "Open the npm page"
434
+ updateOpen: "Open the npm page",
435
+ // Session-row hover card (`sidebar.session.row.hover`).
436
+ hoverStats: "{size} · {n} files",
437
+ hoverCwd: "Folder",
438
+ hoverArchived: "Archived",
439
+ hoverPinned: "Pinned"
355
440
  };
356
441
  var dictionaries = { zh, en };
357
442
  function text(t, key, params) {
@@ -363,11 +448,36 @@ function text(t, key, params) {
363
448
  }
364
449
  var TAB_LABEL = {
365
450
  archived: "titleArchived",
366
- pinned: "titlePinned",
367
- trash: "titleTrash"
451
+ pinned: "titlePinned"
368
452
  };
369
453
 
454
+ // src/client/snapshot.ts
455
+ var last = null;
456
+ function cachedSnapshot() {
457
+ return last;
458
+ }
459
+ function rememberSnapshot(value) {
460
+ last = value;
461
+ }
462
+ function forgetSnapshot() {
463
+ last = null;
464
+ }
465
+
370
466
  // src/client/styles.ts
467
+ var ROW_ICON_BUTTON = {
468
+ display: "inline-flex",
469
+ alignItems: "center",
470
+ justifyContent: "center",
471
+ width: "16px",
472
+ height: "16px",
473
+ padding: 0,
474
+ margin: 0,
475
+ border: "none",
476
+ background: "transparent",
477
+ borderRadius: "var(--dsw-radius-xs)",
478
+ cursor: "pointer",
479
+ flex: "none"
480
+ };
371
481
  var S = {
372
482
  column: { display: "flex", flexDirection: "column", gap: "10px", padding: "4px 0", minWidth: 0 },
373
483
  rowLine: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" },
@@ -386,12 +496,20 @@ var S = {
386
496
  borderTop: "1px solid var(--dsw-alias-border-l1)"
387
497
  },
388
498
  itemMain: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0, flex: "1 1 auto" },
499
+ /**
500
+ * 标题那一行:标题 + 状态胶囊(「运行中」)。
501
+ *
502
+ * 胶囊 `flex: 0 0 auto`、标题 `minWidth: 0` + 省略号 —— 长标题只挤自己那一段,
503
+ * 不会把胶囊压成两行(与分组标题那次"长路径挤坏按钮"同一条教训,见 groupAction)。
504
+ */
505
+ itemTitleRow: { display: "flex", alignItems: "center", gap: "6px", minWidth: 0 },
389
506
  itemTitle: {
390
507
  color: "var(--dsw-alias-label-primary)",
391
508
  fontSize: "13px",
392
509
  overflow: "hidden",
393
510
  textOverflow: "ellipsis",
394
- whiteSpace: "nowrap"
511
+ whiteSpace: "nowrap",
512
+ minWidth: 0
395
513
  },
396
514
  itemMeta: {
397
515
  color: "var(--dsw-alias-label-secondary)",
@@ -400,6 +518,26 @@ var S = {
400
518
  textOverflow: "ellipsis",
401
519
  whiteSpace: "nowrap"
402
520
  },
521
+ /** 状态胶囊:与 hover 卡片那几个同款(带 alpha 的叠加色,深浅主题都有效)。 */
522
+ badge: {
523
+ color: "var(--dsw-alias-label-secondary)",
524
+ fontSize: "10px",
525
+ lineHeight: "16px",
526
+ padding: "0 6px",
527
+ borderRadius: "8px",
528
+ background: "var(--dsw-alias-interactive-bg-active)",
529
+ flex: "0 0 auto"
530
+ },
531
+ /** 「运行中」:同款胶囊,但用业务蓝表示"它是活的",不是危险。 */
532
+ badgeRunning: {
533
+ color: "var(--dsw-alias-state-business-primary)",
534
+ fontSize: "10px",
535
+ lineHeight: "16px",
536
+ padding: "0 6px",
537
+ borderRadius: "8px",
538
+ background: "var(--dsw-alias-interactive-bg-active)",
539
+ flex: "0 0 auto"
540
+ },
403
541
  itemActions: { display: "flex", alignItems: "center", gap: "6px", flex: "0 0 auto" },
404
542
  /**
405
543
  * 「⋯」触发器:图标按钮,弱化成次要前景色(跟侧边栏行的图标按钮一致)。
@@ -472,6 +610,48 @@ var S = {
472
610
  empty: { color: "var(--dsw-alias-label-secondary)", fontSize: "12px", padding: "14px 10px", textAlign: "center" },
473
611
  error: { color: "var(--dsw-alias-state-error-primary)", fontSize: "12px" },
474
612
  footer: { color: "var(--dsw-alias-label-secondary)", fontSize: "11px", display: "flex", gap: "10px", flexWrap: "wrap" },
613
+ /**
614
+ * 数据页签上的过滤工具条:搜索框在左、时间档在右。
615
+ *
616
+ * `flexWrap` 是必需的:设置页本身可能很窄(侧边栏展开时尤其),搜索框的
617
+ * `minWidth` 兜住它不被压成一条线,挤不下时时间档换行,而不是把输入框挤没。
618
+ */
619
+ filterBar: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" },
620
+ /** 搜索框自己撑开剩余宽度;给个下限,窄屏下也不至于只剩图标。 */
621
+ searchInput: { flex: "1 1 auto", minWidth: "160px" },
622
+ /** 把时间档推到右边(搜索框与它之间那段空白)。 */
623
+ filterSpacer: { flex: "1 1 auto" },
624
+ // ── 磁盘占用视图(配置页里的一块) ─────────────────────────────────────
625
+ usageBody: { display: "flex", flexDirection: "column", padding: "0 12px 12px", gap: "2px" },
626
+ usageHeading: {
627
+ color: "var(--dsw-alias-label-tertiary)",
628
+ fontSize: "11px",
629
+ marginTop: "8px",
630
+ marginBottom: "2px"
631
+ },
632
+ /** 一条工作区横条:轨道 + 名字 + 数字。名字可以很长,所以它自己省略。 */
633
+ usageBarRow: { display: "flex", alignItems: "center", gap: "8px", minWidth: 0 },
634
+ usageBarTrack: {
635
+ width: "70px",
636
+ height: "6px",
637
+ borderRadius: "3px",
638
+ background: "var(--dsw-alias-interactive-bg-active)",
639
+ flex: "0 0 auto",
640
+ overflow: "hidden"
641
+ },
642
+ usageBarFill: { display: "block", height: "100%", background: "var(--dsw-alias-state-business-primary)" },
643
+ usageBarName: {
644
+ color: "var(--dsw-alias-label-primary)",
645
+ fontSize: "12px",
646
+ overflow: "hidden",
647
+ textOverflow: "ellipsis",
648
+ whiteSpace: "nowrap",
649
+ minWidth: 0,
650
+ flex: "1 1 auto"
651
+ },
652
+ usageBarMeta: { color: "var(--dsw-alias-label-secondary)", fontSize: "11px", flex: "0 0 auto" },
653
+ /** 榜单里的一行:与列表行同一套几何,只是没有勾选框。 */
654
+ usageRow: { display: "flex", alignItems: "center", gap: "10px", padding: "6px 0", minWidth: 0 },
475
655
  mono: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" },
476
656
  ack: { display: "flex", alignItems: "center", gap: "8px", fontSize: "12px", color: "var(--dsw-alias-label-primary)" },
477
657
  /** 顶部条:页签在左,动作靠 `marginInlineStart: auto` 推到右边。 */
@@ -490,6 +670,9 @@ var S = {
490
670
  configLabel: { color: "var(--dsw-alias-label-primary)", fontSize: "13px" },
491
671
  configHint: { color: "var(--dsw-alias-label-tertiary)", fontSize: "11px" },
492
672
  configControl: { display: "flex", alignItems: "center", gap: "8px", flex: "0 0 auto" },
673
+ /** 下拉锚点:文字 + 双向箭头图标,别被挤窄(长标签在按钮里折行是这个仓库踩过的坑)。 */
674
+ dropdownAnchor: { flex: "0 0 auto" },
675
+ dropdownButton: { flex: "0 0 auto", whiteSpace: "nowrap" },
493
676
  configFoot: {
494
677
  display: "flex",
495
678
  alignItems: "center",
@@ -525,7 +708,49 @@ var S = {
525
708
  overflowX: "auto",
526
709
  whiteSpace: "pre"
527
710
  },
528
- link: { color: "var(--dsw-alias-label-primary)", fontSize: "11px" }
711
+ link: { color: "var(--dsw-alias-label-primary)", fontSize: "11px" },
712
+ /**
713
+ * 会话行 hover 卡片里我们那一节。卡片自己的 `hoverContent` 已经是纵向 flex 且有间距,
714
+ * 这里只补一点点上边距,不加分隔线 —— 上面那行是官方画的相对时间。
715
+ */
716
+ hoverBlock: { display: "flex", flexDirection: "column", gap: "3px", marginTop: "2px", minWidth: 0 },
717
+ hoverLine: { display: "flex", alignItems: "center", gap: "6px", minWidth: 0 },
718
+ hoverLabel: { color: "var(--dsw-alias-label-tertiary)", fontSize: "11px", flex: "0 0 auto" },
719
+ hoverMeta: { color: "var(--dsw-alias-label-secondary)", fontSize: "11px", flex: "0 0 auto" },
720
+ /** 原目录可能很长:让它自己省略,别把卡片撑宽。 */
721
+ hoverPath: {
722
+ color: "var(--dsw-alias-label-secondary)",
723
+ fontSize: "11px",
724
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
725
+ overflow: "hidden",
726
+ textOverflow: "ellipsis",
727
+ whiteSpace: "nowrap",
728
+ minWidth: 0,
729
+ flex: "0 1 auto"
730
+ },
731
+ hoverBadges: { display: "flex", alignItems: "center", gap: "4px", flexWrap: "wrap" },
732
+ /** 状态胶囊:底色用带 alpha 的 `interactive-bg-active`(浅色主题里 layer 系列是同一个颜色)。 */
733
+ hoverBadge: {
734
+ color: "var(--dsw-alias-label-secondary)",
735
+ fontSize: "10px",
736
+ lineHeight: "16px",
737
+ padding: "0 6px",
738
+ borderRadius: "8px",
739
+ background: "var(--dsw-alias-interactive-bg-active)",
740
+ flex: "0 0 auto"
741
+ },
742
+ /**
743
+ * 侧边栏会话行 hover 时那个「删除会话」图标按钮(见
744
+ * `components/delete-row-button.ts`):16×16 的朴素按钮,**两种状态都没有底色**,
745
+ * 只有前景色变深一档。
746
+ *
747
+ * `color-mix` 是 DSH 自己的产物里就在用的写法(例如 ui-deliverables 的
748
+ * `color-mix(in srgb, var(--dsw-alias-interactive-bg-hover) 50%, transparent)`)。
749
+ * 为什么不换 `state-error-secondary`:那个 token 在浅色主题里是 red-400,比 primary
750
+ * (red-600)**更浅**,方向正好相反。
751
+ */
752
+ rowIconButton: { ...ROW_ICON_BUTTON, color: "var(--dsw-alias-state-error-primary)" },
753
+ rowIconButtonHover: { ...ROW_ICON_BUTTON, color: "color-mix(in srgb, var(--dsw-alias-state-error-primary) 78%, #000)" }
529
754
  };
530
755
 
531
756
  // src/client/components/dialog-host.ts
@@ -541,10 +766,9 @@ function DialogHost(props) {
541
766
  setError(null);
542
767
  }, [requestKey]);
543
768
  if (request === null) return null;
544
- const purge = request.kind === "purge";
545
769
  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 });
770
+ const title = t("confirmDeleteTitle");
771
+ const body = many ? text(t, "confirmDeleteManyBody", { n: request.ids.length }) : text(t, "confirmDeleteBody", { title: request.title });
548
772
  const close = () => {
549
773
  if (busy) return;
550
774
  dialogStore.set(null);
@@ -552,10 +776,11 @@ function DialogHost(props) {
552
776
  const confirm = async () => {
553
777
  setBusy(true);
554
778
  try {
555
- const value = await call(purge ? "purge" : "trash", { ids: request.ids });
779
+ const value = await call("delete", { ids: request.ids });
556
780
  const failure = firstFailure(value.results);
557
781
  if (failure === null) {
558
782
  dialogStore.set(null);
783
+ rememberSnapshot(value);
559
784
  mutationStore.set(mutationStore.get() + 1);
560
785
  } else {
561
786
  setError(failure);
@@ -578,15 +803,19 @@ function DialogHost(props) {
578
803
  "div",
579
804
  { style: S.rowLine },
580
805
  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"))
806
+ React2.createElement(
807
+ import_dsh_client_ui_primitives.Button,
808
+ {
809
+ size: "md",
810
+ variant: "primary",
811
+ disabled: busy || !ack,
812
+ onClick: () => void confirm()
813
+ },
814
+ t("confirmDeleteAction")
815
+ )
587
816
  )
588
817
  },
589
- purge ? React2.createElement(
818
+ React2.createElement(
590
819
  "div",
591
820
  { style: S.column },
592
821
  React2.createElement(
@@ -598,16 +827,44 @@ function DialogHost(props) {
598
827
  disabled: busy,
599
828
  onChange: (event) => setAck(Boolean(event.target.checked))
600
829
  }),
601
- t("ackPurge")
830
+ t("ackDelete")
602
831
  ),
603
832
  error ? React2.createElement("div", { style: S.error }, error) : null
604
- ) : error ? React2.createElement("div", { style: S.error }, error) : null
833
+ )
605
834
  );
606
835
  }
607
836
 
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");
837
+ // src/client/components/session-hover.ts
838
+ var React3 = __toESM(require("react"), 1);
839
+
840
+ // src/client/detail.ts
841
+ var TTL_MS = 3e4;
842
+ var cache = /* @__PURE__ */ new Map();
843
+ var inflight = /* @__PURE__ */ new Map();
844
+ var watching = false;
845
+ function forgetDetails() {
846
+ cache.clear();
847
+ }
848
+ function watchMutations() {
849
+ if (watching) return;
850
+ watching = true;
851
+ mutationStore.subscribe(forgetDetails);
852
+ }
853
+ async function describeSession(sessionId) {
854
+ watchMutations();
855
+ const cached = cache.get(sessionId);
856
+ if (cached !== void 0 && Date.now() - cached.at < TTL_MS) return cached.value;
857
+ const running = inflight.get(sessionId);
858
+ if (running !== void 0) return running;
859
+ const request = call("describe", { ids: [sessionId] }).then((value) => {
860
+ cache.set(sessionId, { at: Date.now(), value });
861
+ return value;
862
+ }).finally(() => {
863
+ inflight.delete(sessionId);
864
+ });
865
+ inflight.set(sessionId, request);
866
+ return request;
867
+ }
611
868
 
612
869
  // src/client/format.ts
613
870
  function formatBytes(bytes) {
@@ -635,7 +892,121 @@ function baseName(path) {
635
892
  return parts.length > 0 ? parts[parts.length - 1] : path;
636
893
  }
637
894
 
895
+ // src/client/components/session-hover.ts
896
+ function SessionHover(props) {
897
+ const t = props.t;
898
+ const [detail, setDetail] = React3.useState(null);
899
+ React3.useEffect(() => {
900
+ let live = true;
901
+ describeSession(props.sessionId).then(
902
+ (value) => {
903
+ if (live) setDetail(value);
904
+ },
905
+ () => {
906
+ }
907
+ );
908
+ return () => {
909
+ live = false;
910
+ };
911
+ }, [props.sessionId]);
912
+ if (detail === null) return null;
913
+ const badges = [];
914
+ if (detail.archived) badges.push({ key: "archived", label: t("hoverArchived") });
915
+ if (detail.pinned) badges.push({ key: "pinned", label: t("hoverPinned") });
916
+ const lines = [
917
+ React3.createElement(
918
+ "div",
919
+ { key: "stats", style: S.hoverLine },
920
+ React3.createElement("span", { style: S.hoverMeta }, text(t, "hoverStats", { size: formatBytes(detail.bytes), n: detail.files }))
921
+ )
922
+ ];
923
+ if (detail.cwd) {
924
+ lines.push(
925
+ React3.createElement(
926
+ "div",
927
+ { key: "cwd", style: S.hoverLine },
928
+ React3.createElement("span", { style: S.hoverLabel }, t("hoverCwd")),
929
+ React3.createElement("span", { style: S.hoverPath, title: detail.cwd }, detail.cwd)
930
+ )
931
+ );
932
+ }
933
+ if (badges.length > 0) {
934
+ lines.push(
935
+ React3.createElement(
936
+ "div",
937
+ { key: "badges", style: S.hoverBadges },
938
+ badges.map(
939
+ (badge) => React3.createElement(
940
+ "span",
941
+ {
942
+ key: badge.key,
943
+ style: S.hoverBadge
944
+ },
945
+ badge.label
946
+ )
947
+ )
948
+ )
949
+ );
950
+ }
951
+ return React3.createElement("div", { style: S.hoverBlock }, lines);
952
+ }
953
+
954
+ // src/client/components/settings-panel.ts
955
+ var React8 = __toESM(require("react"), 1);
956
+ var import_dsh_client_ui_primitives6 = require("@deepseek-ai/dsh-client-ui-primitives");
957
+
958
+ // src/client/export.ts
959
+ function openSessionPath(sessionId, reveal) {
960
+ return call("open", reveal === true ? { ids: [sessionId], target: "reveal" } : { ids: [sessionId], target: "path" });
961
+ }
962
+ async function copyText(value) {
963
+ const clipboard = globalThis.navigator?.clipboard;
964
+ if (clipboard !== void 0 && typeof clipboard.writeText === "function") {
965
+ try {
966
+ await clipboard.writeText(value);
967
+ return true;
968
+ } catch {
969
+ }
970
+ }
971
+ try {
972
+ const document = globalThis.document;
973
+ if (document === void 0) return false;
974
+ const area = document.createElement("textarea");
975
+ area.value = value;
976
+ area.style.position = "fixed";
977
+ area.style.top = "-1000px";
978
+ area.style.opacity = "0";
979
+ document.body.appendChild(area);
980
+ area.select();
981
+ const copied = document.execCommand("copy");
982
+ document.body.removeChild(area);
983
+ return copied;
984
+ } catch {
985
+ return false;
986
+ }
987
+ }
988
+
638
989
  // src/client/groups.ts
990
+ function workspaceSummaries(archived, pinned) {
991
+ const summaries = /* @__PURE__ */ new Map();
992
+ const bucketOf = (key) => {
993
+ let summary = summaries.get(key);
994
+ if (summary === void 0) {
995
+ summary = { key, archivableIds: [], deletableIds: [], activeIds: [] };
996
+ summaries.set(key, summary);
997
+ }
998
+ return summary;
999
+ };
1000
+ for (const rows of [archived, pinned]) {
1001
+ for (const row of rows) {
1002
+ const summary = bucketOf(groupKeyOf(row));
1003
+ summary.archivableIds.push(row.sessionId);
1004
+ summary.deletableIds.push(row.sessionId);
1005
+ if (row.active === true) summary.activeIds.push(row.sessionId);
1006
+ }
1007
+ }
1008
+ return summaries;
1009
+ }
639
1010
  function groupKeyOf(row) {
640
1011
  if (typeof row.cwd === "string" && row.cwd.trim() !== "") {
641
1012
  return `cwd:${row.cwd.trim().replace(/[\\/]+$/, "").replace(/\\/g, "/").toLowerCase()}`;
@@ -667,14 +1038,50 @@ function groupRows(rows, t) {
667
1038
  return list;
668
1039
  }
669
1040
  function sortRows(rows, order) {
1041
+ const descending = (pick) => (left, right) => {
1042
+ const a = pick(left);
1043
+ const b = pick(right);
1044
+ const hasA = typeof a === "number" && Number.isFinite(a) && a > 0;
1045
+ const hasB = typeof b === "number" && Number.isFinite(b) && b > 0;
1046
+ if (!hasA && !hasB) return 0;
1047
+ if (!hasA) return 1;
1048
+ if (!hasB) return -1;
1049
+ return b - a;
1050
+ };
670
1051
  if (order === "title") {
671
1052
  return [...rows].sort((left, right) => String(left.title ?? "").localeCompare(String(right.title ?? "")));
672
1053
  }
673
1054
  if (order === "bytes") {
674
1055
  return [...rows].sort((left, right) => (right.bytes ?? 0) - (left.bytes ?? 0));
675
1056
  }
1057
+ if (order === "createdAt") return [...rows].sort(descending((row) => row.createdAt));
676
1058
  return rows;
677
1059
  }
1060
+ var RECENT_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
1061
+ function matchesSearch(row, search) {
1062
+ const needle = search.trim().toLowerCase();
1063
+ if (needle === "") return true;
1064
+ const haystacks = [row.title, row.cwd, row.workspaceTitle];
1065
+ return haystacks.some((value) => typeof value === "string" && value.toLowerCase().includes(needle));
1066
+ }
1067
+ function timeStampOf(row) {
1068
+ if (typeof row.createdAt === "number" && row.createdAt > 0) return row.createdAt;
1069
+ return null;
1070
+ }
1071
+ function matchesFilter(row, filter) {
1072
+ if (!matchesSearch(row, filter.search)) return false;
1073
+ if (filter.timeFilter !== "recent") return true;
1074
+ const stamp = timeStampOf(row);
1075
+ if (stamp === null) return false;
1076
+ return (filter.now ?? Date.now()) - stamp <= RECENT_WINDOW_MS;
1077
+ }
1078
+ function filterRows(rows, filter) {
1079
+ if (filter.search.trim() === "" && filter.timeFilter === "all") return rows;
1080
+ return rows.filter((row) => matchesFilter(row, filter));
1081
+ }
1082
+ function isFilterActive(filter) {
1083
+ return filter.search.trim() !== "" || filter.timeFilter !== "all";
1084
+ }
678
1085
  function collapseKey(tab, groupKey) {
679
1086
  return `${tab}:${groupKey}`;
680
1087
  }
@@ -698,8 +1105,11 @@ var DEFAULT_SETTINGS = {
698
1105
  groupDefault: "expanded",
699
1106
  showMeta: true,
700
1107
  order: "host",
701
- autoUpdate: true
1108
+ autoUpdate: true,
1109
+ search: "",
1110
+ timeFilter: "all"
702
1111
  };
1112
+ var SEARCH_MAX_LENGTH = 200;
703
1113
  var SETTINGS_KEY = "dsh-session-curator.settings";
704
1114
  function readSettings() {
705
1115
  try {
@@ -712,12 +1122,14 @@ function readSettings() {
712
1122
  const value = validate(parsed[key]);
713
1123
  if (value !== void 0) next[key] = value;
714
1124
  };
715
- pick("tab", (value) => value === "archived" || value === "pinned" || value === "trash" ? value : void 0);
1125
+ pick("tab", (value) => value === "archived" || value === "pinned" ? value : void 0);
716
1126
  pick("grouping", (value) => typeof value === "boolean" ? value : void 0);
717
1127
  pick("groupDefault", (value) => value === "expanded" || value === "collapsed" ? value : void 0);
718
1128
  pick("showMeta", (value) => typeof value === "boolean" ? value : void 0);
719
- pick("order", (value) => value === "host" || value === "title" || value === "bytes" ? value : void 0);
1129
+ pick("order", (value) => value === "host" || value === "title" || value === "bytes" || value === "createdAt" ? value : void 0);
720
1130
  pick("autoUpdate", (value) => typeof value === "boolean" ? value : void 0);
1131
+ pick("search", (value) => typeof value === "string" ? value.slice(0, SEARCH_MAX_LENGTH) : void 0);
1132
+ pick("timeFilter", (value) => value === "all" || value === "recent" ? value : void 0);
721
1133
  return next;
722
1134
  } catch {
723
1135
  return { ...DEFAULT_SETTINGS };
@@ -726,6 +1138,7 @@ function readSettings() {
726
1138
  var settingsStore = createStore(readSettings());
727
1139
  function updateSettings(patch) {
728
1140
  const next = { ...settingsStore.get(), ...patch };
1141
+ if (typeof next.search === "string" && next.search.length > SEARCH_MAX_LENGTH) next.search = next.search.slice(0, SEARCH_MAX_LENGTH);
729
1142
  settingsStore.set(next);
730
1143
  try {
731
1144
  if (globalThis.localStorage) globalThis.localStorage.setItem(SETTINGS_KEY, JSON.stringify(next));
@@ -760,6 +1173,18 @@ function writeSidebarArchived(show) {
760
1173
  }
761
1174
  }
762
1175
 
1176
+ // src/client/toast.ts
1177
+ var toastStore = createStore(null);
1178
+ var sequence = 0;
1179
+ function showNotice(message) {
1180
+ if (message === "") return;
1181
+ sequence += 1;
1182
+ toastStore.set({ seq: sequence, message });
1183
+ }
1184
+ function dismissToast() {
1185
+ toastStore.set(null);
1186
+ }
1187
+
763
1188
  // src/client/update.ts
764
1189
  var updateStore = createStore({ state: "idle", value: null, error: null });
765
1190
  async function checkUpdate(force) {
@@ -772,19 +1197,64 @@ async function checkUpdate(force) {
772
1197
  }
773
1198
  }
774
1199
 
775
- // src/client/components/group-header.ts
776
- var React3 = __toESM(require("react"), 1);
1200
+ // src/client/components/dropdown.ts
1201
+ var React4 = __toESM(require("react"), 1);
777
1202
  var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
1203
+ function Dropdown(props) {
1204
+ const current2 = props.options.find((option) => option.value === props.value);
1205
+ const display = props.display ?? (current2 === void 0 ? String(props.value) : current2.label);
1206
+ const optionId = (value) => `${props.id}-${String(value)}`;
1207
+ return React4.createElement(import_dsh_client_ui_primitives2.Menu, {
1208
+ open: props.open,
1209
+ portal: true,
1210
+ side: "bottom",
1211
+ align: props.align ?? "end",
1212
+ items: props.options.map((option) => ({
1213
+ id: optionId(option.value),
1214
+ label: option.label,
1215
+ disabled: option.disabled === true
1216
+ })),
1217
+ // 命中当前档位时由 `Menu` 在行**尾**画对勾 —— 这是数据行选中态的唯一来源。
1218
+ selectedId: optionId(props.value),
1219
+ onSelect: (id) => {
1220
+ props.onToggle(false);
1221
+ const picked = props.options.find((option) => optionId(option.value) === id);
1222
+ if (picked !== void 0 && picked.value !== props.value) props.onChange(picked.value);
1223
+ },
1224
+ onClose: () => props.onToggle(false),
1225
+ className: S.dropdownAnchor,
1226
+ anchor: React4.createElement(
1227
+ import_dsh_client_ui_primitives2.Button,
1228
+ {
1229
+ size: "sm",
1230
+ variant: "outline",
1231
+ disabled: props.disabled === true,
1232
+ style: S.dropdownButton,
1233
+ "aria-label": props.label,
1234
+ "aria-haspopup": "menu",
1235
+ "aria-expanded": props.open,
1236
+ icon: React4.createElement(import_dsh_client_ui_primitives2.IconChevronsUpDownOutlineRegular, { size: 14 }),
1237
+ onClick: () => props.onToggle(!props.open)
1238
+ },
1239
+ display
1240
+ )
1241
+ });
1242
+ }
1243
+
1244
+ // src/client/components/group-header.ts
1245
+ var React5 = __toESM(require("react"), 1);
1246
+ var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
778
1247
  function GroupHeader(props) {
779
1248
  const group = props.group;
780
1249
  const t = props.t;
781
1250
  const total = group.rows.length;
782
1251
  const allSelected = total > 0 && group.rows.every((row) => props.selected.includes(row.sessionId));
783
1252
  const action = allSelected ? t("clearShort") : t("selectAllShort");
784
- return React3.createElement(
1253
+ const workspaceActions = props.workspaceActions ?? [];
1254
+ return React5.createElement(
785
1255
  "div",
786
1256
  { style: props.first ? { ...S.groupHead, borderTop: "none" } : S.groupHead },
787
- React3.createElement(
1257
+ React5.createElement(
788
1258
  "button",
789
1259
  {
790
1260
  type: "button",
@@ -792,28 +1262,78 @@ function GroupHeader(props) {
792
1262
  title: props.collapsed ? t("expand") : t("collapse"),
793
1263
  onClick: () => props.onToggleCollapse()
794
1264
  },
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
1265
+ React5.createElement(
1266
+ "span",
1267
+ { style: S.groupCaret },
1268
+ props.collapsed ? React5.createElement(import_dsh_client_ui_primitives3.IconChevronRightOutlineRegular, { size: 12 }) : React5.createElement(import_dsh_client_ui_primitives3.IconChevronDownOutlineRegular, { size: 12 })
1269
+ ),
1270
+ React5.createElement(
1271
+ "span",
1272
+ { style: S.groupIcon },
1273
+ props.collapsed ? React5.createElement(import_dsh_client_ui_primitives3.IconFolderCloseRegular, { size: 14 }) : React5.createElement(import_dsh_client_ui_primitives3.IconFolderOpenRegular, { size: 14 })
1274
+ ),
1275
+ React5.createElement("span", { style: S.groupTitle, title: group.cwd || group.title }, group.title),
1276
+ React5.createElement("span", { style: S.groupMeta }, text(t, "sessions", { n: total })),
1277
+ group.bytes > 0 ? React5.createElement("span", { style: S.groupMeta }, formatBytes(group.bytes)) : null,
1278
+ group.cwd !== null ? React5.createElement("span", { style: { ...S.groupPath, ...S.mono }, title: group.cwd }, group.cwd) : null
801
1279
  ),
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)
1280
+ // 工作区动作:先勾选(本页签),再「⋯」(跨页签)。两个都不参与 flex 收缩,
1281
+ // 长路径挤的只能是左边的折叠按钮(见 styles.ts 的 groupAction)。
1282
+ React5.createElement(
1283
+ import_dsh_client_ui_primitives3.Button,
1284
+ {
1285
+ size: "sm",
1286
+ variant: "ghost",
1287
+ disabled: props.busy,
1288
+ style: S.groupAction,
1289
+ title: `${group.title} — ${allSelected ? t("clearGroup") : t("selectGroup")}`,
1290
+ onClick: () => props.onToggleGroup()
1291
+ },
1292
+ action
1293
+ ),
1294
+ workspaceActions.length === 0 ? null : React5.createElement(import_dsh_client_ui_primitives3.Menu, {
1295
+ open: props.workspaceMenuOpen === true,
1296
+ portal: true,
1297
+ side: "bottom",
1298
+ align: "end",
1299
+ closeOnPointerLeave: true,
1300
+ items: workspaceActions,
1301
+ onSelect: (id) => props.onRunWorkspaceAction?.(id),
1302
+ onClose: () => props.onCloseWorkspaceMenu?.(),
1303
+ anchor: React5.createElement(import_dsh_client_ui_primitives3.Button, {
1304
+ size: "sm",
1305
+ variant: "ghost",
1306
+ disabled: props.busy,
1307
+ style: S.rowMenuButton,
1308
+ icon: React5.createElement(import_dsh_client_ui_primitives3.IconEllipsisOutlineRegular, {}),
1309
+ "aria-label": `${group.title} — ${t("workspaceActions")}`,
1310
+ onClick: () => props.onToggleWorkspaceMenu?.()
1311
+ })
1312
+ })
811
1313
  );
812
1314
  }
1315
+ function workspaceActionsOf(t, summary) {
1316
+ return [
1317
+ {
1318
+ id: "archive",
1319
+ label: t("archiveWorkspace"),
1320
+ icon: React5.createElement(import_dsh_client_ui_primitives3.IconArchiveOutlineRegular, { size: 14 }),
1321
+ // 全都归档过了就没什么可归档的;有会话正在跑时也不给点(归档会先停掉它的工作)。
1322
+ disabled: summary.archivableIds.length === 0 || summary.activeIds.length > 0
1323
+ },
1324
+ {
1325
+ id: "delete",
1326
+ label: t("deleteWorkspace"),
1327
+ icon: React5.createElement(import_dsh_client_ui_primitives3.IconTrashOutlineRegular, { size: 14 }),
1328
+ danger: true,
1329
+ disabled: summary.deletableIds.length === 0 || summary.activeIds.length > 0
1330
+ }
1331
+ ];
1332
+ }
813
1333
 
814
1334
  // 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");
1335
+ var React6 = __toESM(require("react"), 1);
1336
+ var import_dsh_client_ui_primitives4 = require("@deepseek-ai/dsh-client-ui-primitives");
817
1337
  function SessionRow(props) {
818
1338
  const row = props.row;
819
1339
  const t = props.t;
@@ -823,29 +1343,36 @@ function SessionRow(props) {
823
1343
  if (row.bytes) meta.push(formatBytes(row.bytes));
824
1344
  if (row.files) meta.push(text(t, "files", { n: row.files }));
825
1345
  }
826
- const time = props.showDeletedAt ? formatTime(row.deletedAt) : formatTime(row.createdAt);
1346
+ const time = formatTime(row.createdAt);
827
1347
  if (time !== "") meta.push(time);
828
1348
  const menu = props.menu;
829
- return React4.createElement(
1349
+ const running = row.active === true;
1350
+ return React6.createElement(
830
1351
  "div",
831
1352
  { style: props.first ? { ...S.item, borderTop: "none" } : S.item },
832
- React4.createElement("input", {
1353
+ React6.createElement("input", {
833
1354
  type: "checkbox",
834
1355
  checked: props.checked,
835
1356
  disabled: props.busy,
836
1357
  "aria-label": row.title,
837
1358
  onChange: () => props.onToggle()
838
1359
  }),
839
- React4.createElement(
1360
+ React6.createElement(
840
1361
  "div",
841
1362
  { 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(" · "))
1363
+ React6.createElement(
1364
+ "div",
1365
+ { style: S.itemTitleRow },
1366
+ React6.createElement("div", { style: S.itemTitle, title: row.title }, row.title),
1367
+ // 胶囊:`flex: 0 0 auto`,所以长标题只会挤标题那一段(它有 minWidth: 0 + 省略号)。
1368
+ running ? React6.createElement("span", { style: S.badgeRunning, title: t("runningHint") }, t("running")) : null
1369
+ ),
1370
+ React6.createElement("div", { style: S.itemMeta, title: row.cwd || "" }, meta.join(" · "))
844
1371
  ),
845
- React4.createElement(
1372
+ React6.createElement(
846
1373
  "div",
847
1374
  { style: S.itemActions },
848
- React4.createElement(import_dsh_client_ui_primitives3.Menu, {
1375
+ React6.createElement(import_dsh_client_ui_primitives4.Menu, {
849
1376
  open: props.menuOpen,
850
1377
  portal: true,
851
1378
  side: "bottom",
@@ -854,12 +1381,12 @@ function SessionRow(props) {
854
1381
  items: menu.items,
855
1382
  onSelect: (id) => menu.run(id),
856
1383
  onClose: () => props.onCloseMenu(),
857
- anchor: React4.createElement(import_dsh_client_ui_primitives3.Button, {
1384
+ anchor: React6.createElement(import_dsh_client_ui_primitives4.Button, {
858
1385
  size: "sm",
859
1386
  variant: "ghost",
860
1387
  disabled: props.busy,
861
1388
  style: S.rowMenuButton,
862
- icon: React4.createElement(import_dsh_client_ui_primitives3.IconEllipsisOutlineRegular, {}),
1389
+ icon: React6.createElement(import_dsh_client_ui_primitives4.IconEllipsisOutlineRegular, {}),
863
1390
  "aria-label": `${row.title} — ${t("rowActions")}`,
864
1391
  onClick: (event) => {
865
1392
  event.stopPropagation();
@@ -871,27 +1398,144 @@ function SessionRow(props) {
871
1398
  );
872
1399
  }
873
1400
 
1401
+ // src/client/components/usage-panel.ts
1402
+ var React7 = __toESM(require("react"), 1);
1403
+ var import_dsh_client_ui_primitives5 = require("@deepseek-ai/dsh-client-ui-primitives");
1404
+ function usageRow(t, row, busy, onArchive, onUnarchive) {
1405
+ const badges = [];
1406
+ if (row.archived) badges.push({ key: "archived", label: t("usageArchived"), style: S.badge });
1407
+ else if (row.pinned) badges.push({ key: "pinned", label: t("usagePinned"), style: S.badge });
1408
+ if (row.active) badges.push({ key: "running", label: t("running"), style: S.badgeRunning });
1409
+ const action = row.archived ? { label: t("usageUnarchive"), run: () => onUnarchive(row.sessionId) } : row.active ? null : { label: t("usageArchive"), run: () => onArchive(row.sessionId) };
1410
+ const when = formatTime(row.createdAt);
1411
+ return React7.createElement(
1412
+ "div",
1413
+ { key: row.sessionId, style: S.usageRow },
1414
+ React7.createElement(
1415
+ "div",
1416
+ { style: S.itemMain },
1417
+ React7.createElement(
1418
+ "div",
1419
+ { style: S.itemTitleRow },
1420
+ React7.createElement("div", { style: S.itemTitle, title: row.title }, row.title),
1421
+ badges.map((badge) => React7.createElement("span", { key: badge.key, style: badge.style }, badge.label))
1422
+ ),
1423
+ React7.createElement(
1424
+ "div",
1425
+ { style: S.itemMeta, title: row.cwd || "" },
1426
+ [formatBytes(row.bytes), text(t, "files", { n: row.files }), row.workspaceTitle ?? row.cwd ?? t("noProject"), when].filter((part) => part !== "").join(" · ")
1427
+ )
1428
+ ),
1429
+ React7.createElement(
1430
+ "div",
1431
+ { style: S.itemActions },
1432
+ action === null ? null : React7.createElement(
1433
+ import_dsh_client_ui_primitives5.Button,
1434
+ {
1435
+ size: "sm",
1436
+ variant: "outline",
1437
+ disabled: busy,
1438
+ onClick: action.run
1439
+ },
1440
+ action.label
1441
+ )
1442
+ )
1443
+ );
1444
+ }
1445
+ function UsagePanel(props) {
1446
+ const t = props.t;
1447
+ const usage = props.usage;
1448
+ const maxBytes = usage === null || usage.groups.length === 0 ? 0 : Math.max(...usage.groups.map((group) => group.bytes), 1);
1449
+ return React7.createElement(
1450
+ "div",
1451
+ { style: S.card },
1452
+ React7.createElement(
1453
+ "div",
1454
+ { style: { ...S.configRow, borderTop: "none" } },
1455
+ React7.createElement(
1456
+ "div",
1457
+ { style: S.configMain },
1458
+ React7.createElement("span", { style: S.configLabel }, t("usageTitle")),
1459
+ React7.createElement(
1460
+ "span",
1461
+ { style: S.configHint },
1462
+ props.loading ? t("usageLoading") : usage === null ? props.error ?? "" : text(t, "usageTotal", {
1463
+ n: usage.totalSessions,
1464
+ size: formatBytes(usage.totalBytes)
1465
+ })
1466
+ )
1467
+ )
1468
+ ),
1469
+ usage === null || usage.totalSessions === 0 ? React7.createElement("div", { style: S.empty }, props.loading ? t("usageLoading") : t("usageNoData")) : React7.createElement(
1470
+ "div",
1471
+ { style: S.usageBody },
1472
+ // 按工作区:横条 + 名字 + 体积 / 条数。
1473
+ React7.createElement("span", { style: S.usageHeading }, t("usageByWorkspace")),
1474
+ usage.groups.map(
1475
+ (group) => React7.createElement(
1476
+ "div",
1477
+ { key: group.key, style: S.usageBarRow },
1478
+ React7.createElement(
1479
+ "span",
1480
+ { style: S.usageBarTrack },
1481
+ React7.createElement("span", {
1482
+ style: {
1483
+ ...S.usageBarFill,
1484
+ // 占比:按最大组归一,所以最小的组也看得见(下限 2%)。
1485
+ width: `${Math.max(2, Math.round(group.bytes / maxBytes * 100))}%`
1486
+ }
1487
+ })
1488
+ ),
1489
+ React7.createElement(
1490
+ "span",
1491
+ { style: S.usageBarName, title: group.key },
1492
+ group.title === "" ? t("noProject") : group.title
1493
+ ),
1494
+ React7.createElement(
1495
+ "span",
1496
+ { style: S.usageBarMeta },
1497
+ `${formatBytes(group.bytes)} · ${text(t, "sessions", { n: group.count })}`
1498
+ )
1499
+ )
1500
+ ),
1501
+ React7.createElement("span", { style: S.usageHeading }, t("usageLargest")),
1502
+ usage.largest.map((row) => usageRow(t, row, props.busy, props.onArchive, props.onUnarchive)),
1503
+ React7.createElement("span", { style: S.usageHeading }, t("usageOldest")),
1504
+ usage.oldest.map((row) => usageRow(t, row, props.busy, props.onArchive, props.onUnarchive))
1505
+ )
1506
+ );
1507
+ }
1508
+
874
1509
  // src/client/components/settings-panel.ts
875
1510
  function SessionManagerPanel(props) {
876
1511
  const t = props.t;
877
1512
  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);
1513
+ const [tab, setTab] = React8.useState(settings.tab);
1514
+ const [snapshot, setSnapshot] = React8.useState(cachedSnapshot);
1515
+ const [error, setError] = React8.useState(null);
1516
+ const [loading, setLoading] = React8.useState(true);
1517
+ const [busy, setBusy] = React8.useState(false);
1518
+ const [selected, setSelected] = React8.useState([]);
1519
+ const [collapsedOverrides, setCollapsedOverrides] = React8.useState({});
1520
+ const [menuFor, setMenuFor] = React8.useState(null);
1521
+ const [workspaceMenuFor, setWorkspaceMenuFor] = React8.useState(null);
1522
+ const [usage, setUsage] = React8.useState(null);
1523
+ const [usageError, setUsageError] = React8.useState(null);
1524
+ const [usageBusy, setUsageBusy] = React8.useState(false);
1525
+ const [usageReload, setUsageReload] = React8.useState(0);
1526
+ const [openDropdown, setOpenDropdown] = React8.useState(null);
886
1527
  const grouping = useStore(groupingStore);
887
1528
  const revision = useStore(mutationStore);
888
1529
  const update = useStore(updateStore);
889
- const [sidebarArchived, setSidebarArchived] = React5.useState(readSidebarArchived);
890
- const [sidebarAtMount] = React5.useState(readSidebarArchived);
1530
+ const [sidebarArchived, setSidebarArchived] = React8.useState(readSidebarArchived);
1531
+ const [sidebarAtMount] = React8.useState(readSidebarArchived);
891
1532
  const applyResult = (value) => {
892
- if (value && value.archived) setSnapshot(value);
1533
+ if (value && value.archived) {
1534
+ rememberSnapshot(value);
1535
+ setSnapshot(value);
1536
+ }
893
1537
  };
894
- const refresh = React5.useCallback(async () => {
1538
+ const refresh = React8.useCallback(async () => {
895
1539
  setLoading(true);
896
1540
  try {
897
1541
  applyResult(await call("snapshot", {}));
@@ -902,17 +1546,41 @@ function SessionManagerPanel(props) {
902
1546
  setLoading(false);
903
1547
  }
904
1548
  }, []);
905
- React5.useEffect(() => {
1549
+ React8.useEffect(() => {
906
1550
  void refresh();
907
1551
  }, [refresh, revision]);
908
- React5.useEffect(() => {
1552
+ React8.useEffect(() => {
909
1553
  if (tab !== "config" || !settings.autoUpdate || update.state !== "idle") return;
910
1554
  void checkUpdate(false);
911
1555
  }, [tab, settings.autoUpdate, update.state]);
912
- React5.useEffect(() => {
1556
+ React8.useEffect(() => {
913
1557
  setSelected([]);
914
1558
  setMenuFor(null);
1559
+ setWorkspaceMenuFor(null);
915
1560
  }, [tab]);
1561
+ React8.useEffect(() => {
1562
+ if (tab !== "usage") return;
1563
+ let cancelled = false;
1564
+ setUsageBusy(true);
1565
+ setUsageError(null);
1566
+ void call("usage", {}).then((value) => {
1567
+ if (!cancelled) setUsage(value);
1568
+ }).catch((failure) => {
1569
+ if (!cancelled) {
1570
+ setUsage(null);
1571
+ setUsageError(reportError(failure));
1572
+ }
1573
+ }).finally(() => {
1574
+ if (!cancelled) setUsageBusy(false);
1575
+ });
1576
+ return () => {
1577
+ cancelled = true;
1578
+ };
1579
+ }, [tab, revision, usageReload]);
1580
+ const refreshUsage = () => setUsageReload((current2) => current2 + 1);
1581
+ const runUsageAction = (endpoint, sessionId) => {
1582
+ void run(endpoint, [sessionId]).then(() => refreshUsage());
1583
+ };
916
1584
  const run = async (endpoint, ids) => {
917
1585
  if (ids.length === 0) return;
918
1586
  setBusy(true);
@@ -940,354 +1608,616 @@ function SessionManagerPanel(props) {
940
1608
  };
941
1609
  const archived = snapshot ? snapshot.archived : [];
942
1610
  const pinned = snapshot ? snapshot.pinned : [];
943
- const trashed = snapshot ? snapshot.trashed : [];
944
- const rows = sortRows(tab === "archived" ? archived : tab === "pinned" ? pinned : tab === "trash" ? trashed : [], settings.order);
1611
+ const rows = sortRows(tab === "archived" ? archived : pinned, settings.order);
1612
+ const filter = React8.useMemo(
1613
+ () => ({ search: settings.search, timeFilter: settings.timeFilter }),
1614
+ [settings.search, settings.timeFilter]
1615
+ );
1616
+ const filtered = React8.useMemo(() => filterRows(rows, filter), [rows, filter]);
1617
+ const filtering = isFilterActive(filter);
945
1618
  const selectedRows = rows.filter((row) => selected.includes(row.sessionId));
946
- const groups = grouping ? groupRows(rows, t) : [];
1619
+ const groups = grouping ? groupRows(filtered, t) : [];
947
1620
  const blockOf = (group) => {
948
1621
  const key = collapseKey(tab, group.key);
949
1622
  return { key, group, rows: isGroupCollapsed(key, collapsedOverrides, settings.groupDefault) ? [] : group.rows };
950
1623
  };
951
- const blocks = grouping ? groups.map(blockOf) : [{ key: `${tab}:flat`, group: null, rows }];
1624
+ const blocks = grouping ? groups.map(blockOf) : [{ key: `${tab}:flat`, group: null, rows: filtered }];
952
1625
  const tabs = [
953
1626
  { id: "archived", label: t(TAB_LABEL.archived), count: archived.length },
954
1627
  { id: "pinned", label: t(TAB_LABEL.pinned), count: pinned.length },
955
- { id: "trash", label: t(TAB_LABEL.trash), count: trashed.length },
1628
+ { id: "usage", label: t("titleUsage"), count: 0 },
956
1629
  { id: "config", label: t("titleConfig"), count: 0 }
957
1630
  ];
1631
+ const orderOptions = [
1632
+ { value: "host", label: t("orderHost") },
1633
+ { value: "title", label: t("orderTitle") },
1634
+ { value: "bytes", label: t("orderBytes") },
1635
+ { value: "createdAt", label: t("orderCreatedAt") }
1636
+ ];
1637
+ const summaries = React8.useMemo(() => workspaceSummaries(archived, pinned), [archived, pinned]);
1638
+ const copyRowPath = async (sessionId) => {
1639
+ try {
1640
+ const value = await openSessionPath(sessionId);
1641
+ const copied = await copyText(value.path);
1642
+ showNotice(copied ? text(t, "pathCopied", { path: value.path }) : text(t, "pathCopyFailed", { path: value.path }));
1643
+ } catch (failure) {
1644
+ showNotice(reportError(failure));
1645
+ }
1646
+ };
1647
+ const revealRow = async (sessionId) => {
1648
+ try {
1649
+ const value = await openSessionPath(sessionId, true);
1650
+ if (!value.revealed) {
1651
+ await copyText(value.path);
1652
+ showNotice(text(t, "revealFailed", { reason: value.reason ?? "?", path: value.path }));
1653
+ }
1654
+ } catch (failure) {
1655
+ showNotice(reportError(failure));
1656
+ }
1657
+ };
958
1658
  const rowMenu = (row) => {
959
- const moveToTrash = {
960
- id: "trash",
961
- label: t("moveToTrash"),
962
- icon: React5.createElement(import_dsh_client_ui_primitives4.IconTrashOutlineRegular, { size: 14 }),
963
- danger: true
1659
+ const running = row.active === true;
1660
+ const deleteSession = {
1661
+ id: "delete",
1662
+ label: t("deleteSession"),
1663
+ icon: React8.createElement(import_dsh_client_ui_primitives6.IconTrashOutlineRegular, { size: 14 }),
1664
+ danger: true,
1665
+ disabled: running
1666
+ };
1667
+ const locate = [
1668
+ { id: "copy-path", label: t("copyPath"), icon: React8.createElement(import_dsh_client_ui_primitives6.IconCopyOutlineRegular, { size: 14 }) },
1669
+ { id: "reveal", label: t("revealInFolder"), icon: React8.createElement(import_dsh_client_ui_primitives6.IconFolderOpenRegular, { size: 14 }) }
1670
+ ];
1671
+ const handleLocate = (id) => {
1672
+ if (id === "copy-path") {
1673
+ void copyRowPath(row.sessionId);
1674
+ return true;
1675
+ }
1676
+ if (id === "reveal") {
1677
+ void revealRow(row.sessionId);
1678
+ return true;
1679
+ }
1680
+ return false;
964
1681
  };
965
1682
  if (tab === "archived") {
966
1683
  return {
967
1684
  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
971
- ],
972
- run: (id) => {
973
- setMenuFor(null);
974
- if (id === "trash") requestTrash([row.sessionId], row.title);
975
- else void run(id, [row.sessionId]);
976
- }
977
- };
978
- }
979
- if (tab === "pinned") {
980
- return {
981
- items: [
982
- { id: "unpin", label: t("unpin"), icon: React5.createElement(import_dsh_client_ui_primitives4.IconPinOutlineRegular, { size: 14 }) },
983
- moveToTrash
1685
+ { id: "unarchive", label: t("unarchive"), icon: React8.createElement(import_dsh_client_ui_primitives6.IconUnarchiveOutlineRegular, { size: 14 }) },
1686
+ { id: "pin", label: t("pin"), icon: React8.createElement(import_dsh_client_ui_primitives6.IconPinOutlineRegular, { size: 14 }) },
1687
+ deleteSession,
1688
+ ...locate
984
1689
  ],
985
1690
  run: (id) => {
986
1691
  setMenuFor(null);
987
- if (id === "trash") requestTrash([row.sessionId], row.title);
1692
+ if (handleLocate(id)) return;
1693
+ if (id === "delete") requestDelete([row.sessionId], row.title);
988
1694
  else void run(id, [row.sessionId]);
989
1695
  }
990
1696
  };
991
1697
  }
992
1698
  return {
993
1699
  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 }
1700
+ { id: "unpin", label: t("unpin"), icon: React8.createElement(import_dsh_client_ui_primitives6.IconPinOutlineRegular, { size: 14 }) },
1701
+ deleteSession,
1702
+ ...locate
996
1703
  ],
997
1704
  run: (id) => {
998
1705
  setMenuFor(null);
999
- if (id === "purge") requestPurge([row.sessionId], row.title);
1706
+ if (handleLocate(id)) return;
1707
+ if (id === "delete") requestDelete([row.sessionId], row.title);
1000
1708
  else void run(id, [row.sessionId]);
1001
1709
  }
1002
1710
  };
1003
1711
  };
1712
+ const runWorkspaceAction = (group, id) => {
1713
+ setWorkspaceMenuFor(null);
1714
+ const summary = summaries.get(group.key);
1715
+ if (summary === void 0) return;
1716
+ if (id === "archive") void run("archive", summary.archivableIds);
1717
+ if (id === "delete") requestDelete(summary.deletableIds, group.title);
1718
+ };
1004
1719
  const batch = [];
1005
1720
  if (tab === "archived" && selected.length > 0) {
1006
1721
  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 : "") });
1722
+ batch.push({
1723
+ label: t("deleteSession"),
1724
+ variant: "outline",
1725
+ run: () => requestDelete(selected, selectedRows[0] ? selectedRows[0].title : "")
1726
+ });
1008
1727
  }
1009
1728
  if (tab === "pinned" && selected.length > 0) {
1010
1729
  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 : "") });
1012
- }
1013
- 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 : "") });
1730
+ batch.push({
1731
+ label: t("deleteSession"),
1732
+ variant: "outline",
1733
+ run: () => requestDelete(selected, selectedRows[0] ? selectedRows[0].title : "")
1734
+ });
1016
1735
  }
1017
1736
  const totals = snapshot ? snapshot.totals : null;
1018
- const totalBytes = tab === "archived" ? totals ? totals.archivedBytes : 0 : tab === "trash" ? totals ? totals.trashBytes : 0 : 0;
1737
+ const totalBytes = tab === "archived" && totals ? totals.archivedBytes : 0;
1019
1738
  const nodes = [];
1020
1739
  let first = true;
1021
1740
  for (const block of blocks) {
1022
1741
  if (block.group !== null) {
1023
1742
  const group = block.group;
1024
1743
  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
- }));
1744
+ const summary = summaries.get(group.key);
1745
+ nodes.push(
1746
+ React8.createElement(GroupHeader, {
1747
+ key: `group:${key}`,
1748
+ group,
1749
+ t,
1750
+ first,
1751
+ busy,
1752
+ selected,
1753
+ collapsed: isGroupCollapsed(key, collapsedOverrides, settings.groupDefault),
1754
+ onToggleGroup: () => toggleGroup(group),
1755
+ onToggleCollapse: () => toggleCollapse(key),
1756
+ // 「整个工作区」:跨页签的目标集合由 `workspaceSummaries()` 算好。
1757
+ // 每个渲染出来的分组都至少有一行,所以 `summary` 一定有值;`workspaceActionsOf`
1758
+ // 再按"有没有在跑的会话"把危险项置 `disabled`。
1759
+ workspaceActions: summary === void 0 ? [] : workspaceActionsOf(t, summary),
1760
+ workspaceMenuOpen: workspaceMenuFor === group.key,
1761
+ onToggleWorkspaceMenu: () => setWorkspaceMenuFor(workspaceMenuFor === group.key ? null : group.key),
1762
+ onCloseWorkspaceMenu: () => setWorkspaceMenuFor(null),
1763
+ onRunWorkspaceAction: (id) => runWorkspaceAction(group, id)
1764
+ })
1765
+ );
1036
1766
  first = false;
1037
1767
  }
1038
1768
  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
- }));
1769
+ nodes.push(
1770
+ React8.createElement(SessionRow, {
1771
+ key: row.sessionId,
1772
+ row,
1773
+ t,
1774
+ first,
1775
+ busy,
1776
+ showCwd: block.group === null,
1777
+ showMeta: settings.showMeta,
1778
+ checked: selected.includes(row.sessionId),
1779
+ onToggle: () => toggle(row.sessionId),
1780
+ menu: rowMenu(row),
1781
+ menuOpen: menuFor === row.sessionId,
1782
+ onToggleMenu: () => setMenuFor(menuFor === row.sessionId ? null : row.sessionId),
1783
+ onCloseMenu: () => setMenuFor(null)
1784
+ })
1785
+ );
1055
1786
  first = false;
1056
1787
  }
1057
1788
  }
1058
1789
  const isConfig = tab === "config";
1059
- const configRow = (key, label, hint, control, firstRow) => React5.createElement(
1790
+ const isUsage = tab === "usage";
1791
+ const isDataTab = !isConfig && !isUsage;
1792
+ const configRow = (key, label, hint, control, firstRow) => React8.createElement(
1060
1793
  "div",
1061
1794
  { key, style: firstRow ? { ...S.configRow, borderTop: "none" } : S.configRow },
1062
- React5.createElement(
1795
+ React8.createElement(
1063
1796
  "div",
1064
1797
  { style: S.configMain },
1065
- React5.createElement("span", { style: S.configLabel }, label),
1066
- hint === null ? null : React5.createElement("span", { style: S.configHint }, hint)
1798
+ React8.createElement("span", { style: S.configLabel }, label),
1799
+ hint === null ? null : React8.createElement("span", { style: S.configHint }, hint)
1067
1800
  ),
1068
- React5.createElement("div", { style: S.configControl }, control)
1801
+ React8.createElement("div", { style: S.configControl }, control)
1802
+ );
1803
+ const filterBar = React8.createElement(
1804
+ "div",
1805
+ { style: S.filterBar },
1806
+ React8.createElement(import_dsh_client_ui_primitives6.Input, {
1807
+ value: settings.search,
1808
+ placeholder: t("searchPlaceholder"),
1809
+ "aria-label": t("searchLabel"),
1810
+ icon: React8.createElement(import_dsh_client_ui_primitives6.IconSearchOutlineRegular, {}),
1811
+ style: S.searchInput,
1812
+ onChange: (event) => void updateSettings({ search: event.target.value })
1813
+ }),
1814
+ filtering ? React8.createElement(
1815
+ import_dsh_client_ui_primitives6.Button,
1816
+ {
1817
+ size: "sm",
1818
+ variant: "ghost",
1819
+ onClick: () => void updateSettings({ search: "", timeFilter: "all" })
1820
+ },
1821
+ t("searchClear")
1822
+ ) : null,
1823
+ React8.createElement("div", { style: S.filterSpacer }),
1824
+ React8.createElement(import_dsh_client_ui_primitives6.SegmentedControl, {
1825
+ id: "session-curator-time-filter",
1826
+ value: settings.timeFilter,
1827
+ options: [
1828
+ { value: "all", label: t("timeAll") },
1829
+ { value: "recent", label: t("timeRecent") }
1830
+ ],
1831
+ label: t("timeFilterLabel"),
1832
+ onChange: (next) => void updateSettings({ timeFilter: next })
1833
+ })
1069
1834
  );
1070
1835
  const version = snapshot !== null && typeof snapshot.version === "string" ? snapshot.version : update.value !== null && typeof update.value.current === "string" ? update.value.current : null;
1071
1836
  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(
1837
+ const updateLine = update.state === "checking" ? React8.createElement("span", { style: S.configHint }, t("updateChecking")) : update.state === "failed" ? React8.createElement(
1838
+ "span",
1839
+ { style: S.error },
1840
+ text(t, "updateFailed", { reason: update.error === null ? "?" : update.error })
1841
+ ) : 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;
1842
+ const configPage = React8.createElement(
1074
1843
  "div",
1075
1844
  { style: S.column },
1076
- React5.createElement(
1845
+ React8.createElement(
1077
1846
  "div",
1078
1847
  { 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
- })),
1848
+ configRow(
1849
+ "config:tab",
1850
+ t("configTab"),
1851
+ t("configTabHint"),
1852
+ React8.createElement(import_dsh_client_ui_primitives6.SegmentedControl, {
1853
+ id: "session-curator-default-tab",
1854
+ value: settings.tab,
1855
+ options: [
1856
+ { value: "archived", label: t(TAB_LABEL.archived) },
1857
+ { value: "pinned", label: t(TAB_LABEL.pinned) }
1858
+ ],
1859
+ label: t("configTab"),
1860
+ onChange: (next) => void updateSettings({ tab: next })
1861
+ }),
1862
+ true
1863
+ ),
1864
+ configRow(
1865
+ "config:grouping",
1866
+ t("configGrouping"),
1867
+ t("configGroupingHint"),
1868
+ React8.createElement(import_dsh_client_ui_primitives6.Switch, {
1869
+ checked: settings.grouping,
1870
+ label: t("configGrouping"),
1871
+ onChange: (next) => {
1872
+ updateSettings({ grouping: next });
1873
+ groupingStore.set(next);
1874
+ }
1875
+ })
1876
+ ),
1877
+ configRow(
1878
+ "config:group-default",
1879
+ t("configGroupDefault"),
1880
+ t("configGroupDefaultHint"),
1881
+ React8.createElement(import_dsh_client_ui_primitives6.SegmentedControl, {
1882
+ id: "session-curator-group-default",
1883
+ value: settings.groupDefault,
1884
+ options: [
1885
+ { value: "expanded", label: t("groupExpanded") },
1886
+ { value: "collapsed", label: t("groupCollapsed") }
1887
+ ],
1888
+ label: t("configGroupDefault"),
1889
+ onChange: (next) => void updateSettings({ groupDefault: next })
1890
+ })
1891
+ ),
1892
+ configRow(
1893
+ "config:sidebar-archived",
1894
+ t("configSidebarArchived"),
1895
+ t("configSidebarArchivedHint"),
1896
+ React8.createElement(import_dsh_client_ui_primitives6.Switch, {
1897
+ checked: sidebarArchived !== "default",
1898
+ label: t("configSidebarArchived"),
1899
+ onChange: (next) => {
1900
+ writeSidebarArchived(next);
1901
+ setSidebarArchived(next ? "show" : "default");
1902
+ }
1903
+ })
1904
+ ),
1116
1905
  // 官方 store 只在创建时读一次 localStorage,所以改完必须刷新页面。
1117
- sidebarArchived === sidebarAtMount ? null : React5.createElement(
1906
+ sidebarArchived === sidebarAtMount ? null : React8.createElement(
1118
1907
  "div",
1119
1908
  { 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 {
1909
+ React8.createElement("span", { style: S.configHint }, t("configSidebarRefreshHint")),
1910
+ React8.createElement(
1911
+ import_dsh_client_ui_primitives6.Button,
1912
+ {
1913
+ size: "sm",
1914
+ variant: "outline",
1915
+ onClick: () => {
1916
+ try {
1917
+ globalThis.location.reload();
1918
+ } catch {
1919
+ }
1128
1920
  }
1129
- }
1130
- }, t("refreshPage"))
1921
+ },
1922
+ t("refreshPage")
1923
+ )
1131
1924
  ),
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(
1925
+ configRow(
1926
+ "config:meta",
1927
+ t("configMeta"),
1928
+ t("configMetaHint"),
1929
+ React8.createElement(import_dsh_client_ui_primitives6.Switch, {
1930
+ checked: settings.showMeta,
1931
+ label: t("configMeta"),
1932
+ onChange: (next) => void updateSettings({ showMeta: next })
1933
+ })
1934
+ ),
1935
+ // 排序:5 档,所以是下拉(`Dropdown` 见 components/dropdown.ts)。
1936
+ configRow(
1937
+ "config:order",
1938
+ t("configOrder"),
1939
+ t("configOrderHint"),
1940
+ React8.createElement(Dropdown, {
1941
+ id: "session-curator-order",
1942
+ value: settings.order,
1943
+ options: orderOptions,
1944
+ label: t("configOrder"),
1945
+ open: openDropdown === "order",
1946
+ onToggle: (next) => setOpenDropdown(next ? "order" : null),
1947
+ onChange: (next) => void updateSettings({ order: next })
1948
+ })
1949
+ ),
1950
+ configRow(
1951
+ "config:auto",
1952
+ t("configAutoUpdate"),
1953
+ t("configAutoUpdateHint"),
1954
+ React8.createElement(import_dsh_client_ui_primitives6.Switch, {
1955
+ checked: settings.autoUpdate,
1956
+ label: t("configAutoUpdate"),
1957
+ onChange: (next) => void updateSettings({ autoUpdate: next })
1958
+ })
1959
+ ),
1960
+ React8.createElement(
1154
1961
  "div",
1155
1962
  { 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"))
1963
+ React8.createElement(
1964
+ import_dsh_client_ui_primitives6.Button,
1965
+ {
1966
+ size: "sm",
1967
+ variant: "outline",
1968
+ onClick: () => {
1969
+ updateSettings({ ...DEFAULT_SETTINGS });
1970
+ groupingStore.set(DEFAULT_SETTINGS.grouping);
1971
+ }
1972
+ },
1973
+ t("configReset")
1974
+ )
1164
1975
  )
1165
1976
  ),
1166
- React5.createElement(
1977
+ React8.createElement(
1167
1978
  "div",
1168
1979
  { style: S.card },
1169
- React5.createElement(
1980
+ React8.createElement(
1170
1981
  "div",
1171
1982
  { style: { ...S.configRow, borderTop: "none" } },
1172
- React5.createElement(
1983
+ React8.createElement(
1173
1984
  "div",
1174
1985
  { style: S.configMain },
1175
- React5.createElement("span", { style: S.configLabel }, t("aboutTitle")),
1176
- React5.createElement("span", { style: S.configHint }, "dsh-session-curator")
1986
+ React8.createElement("span", { style: S.configLabel }, t("aboutTitle")),
1987
+ React8.createElement("span", { style: S.configHint }, "dsh-session-curator")
1177
1988
  ),
1178
- React5.createElement(
1989
+ React8.createElement(
1179
1990
  "div",
1180
1991
  { style: S.configControl },
1181
1992
  // 版本号:浅色小字。
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"))
1993
+ React8.createElement("span", { style: S.version }, `${t("versionLabel")} ${version === null ? "—" : `v${version}`}`),
1994
+ React8.createElement(
1995
+ import_dsh_client_ui_primitives6.Button,
1996
+ {
1997
+ size: "sm",
1998
+ variant: "outline",
1999
+ disabled: update.state === "checking",
2000
+ onClick: () => void checkUpdate(true)
2001
+ },
2002
+ t("updateCheckNow")
2003
+ )
1189
2004
  )
1190
2005
  ),
1191
- updateLine === null ? null : React5.createElement("div", { key: "config:update", style: S.configFoot }, updateLine),
1192
- outdated && update.value !== null ? React5.createElement(
2006
+ updateLine === null ? null : React8.createElement("div", { key: "config:update", style: S.configFoot }, updateLine),
2007
+ outdated && update.value !== null ? React8.createElement(
1193
2008
  "div",
1194
2009
  { 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(
2010
+ React8.createElement(
2011
+ "span",
2012
+ { style: S.updateTitle },
2013
+ text(t, "updateAvailable", { latest: update.value.latest ?? "?", current: version === null ? "?" : version })
2014
+ ),
2015
+ React8.createElement(
1197
2016
  "span",
1198
2017
  { 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"))
2018
+ React8.createElement("span", null, t("updateStepsTitle")),
2019
+ React8.createElement("span", null, t("updateStepOne")),
2020
+ React8.createElement("code", { style: S.code }, "dsh plugin --profile desktop add dsh-session-curator@latest"),
2021
+ React8.createElement("span", null, t("updateStepTwo"))
1203
2022
  ),
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"))
2023
+ React8.createElement(
2024
+ "a",
2025
+ {
2026
+ style: S.link,
2027
+ href: "https://www.npmjs.com/package/dsh-session-curator",
2028
+ target: "_blank",
2029
+ rel: "noreferrer"
2030
+ },
2031
+ t("updateOpen")
2032
+ )
1210
2033
  ) : null
1211
2034
  )
1212
2035
  );
1213
- return React5.createElement(
2036
+ const usagePage = React8.createElement(
1214
2037
  "div",
1215
2038
  { style: S.column },
1216
- React5.createElement(
2039
+ React8.createElement(
2040
+ "div",
2041
+ { style: S.rowLine },
2042
+ React8.createElement("div", { style: S.filterSpacer }),
2043
+ React8.createElement(
2044
+ import_dsh_client_ui_primitives6.Button,
2045
+ {
2046
+ size: "sm",
2047
+ variant: "outline",
2048
+ disabled: usageBusy,
2049
+ onClick: () => void refreshUsage()
2050
+ },
2051
+ t("refresh")
2052
+ )
2053
+ ),
2054
+ React8.createElement(UsagePanel, {
2055
+ t,
2056
+ usage,
2057
+ loading: usageBusy,
2058
+ error: usageError,
2059
+ busy,
2060
+ // 榜单上的动作走与列表同一个端点(`run()` 会顺手刷新快照),但**完事之后还要
2061
+ // 重拉一次 `usage`**:`run()` 只换快照,`usage` 不在快照里 —— 不重拉的话
2062
+ // 刚点了「取消归档」的那一行仍然顶着「已归档」胶囊,看起来像没生效。
2063
+ onArchive: (sessionId) => runUsageAction("archive", sessionId),
2064
+ onUnarchive: (sessionId) => runUsageAction("unarchive", sessionId)
2065
+ })
2066
+ );
2067
+ return React8.createElement(
2068
+ "div",
2069
+ { style: S.column },
2070
+ React8.createElement(
1217
2071
  "div",
1218
2072
  { style: S.header },
1219
- React5.createElement(
2073
+ React8.createElement(
1220
2074
  "div",
1221
2075
  { 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})` : ""}`))
2076
+ tabs.map(
2077
+ (entry) => React8.createElement(
2078
+ import_dsh_client_ui_primitives6.Button,
2079
+ {
2080
+ key: entry.id,
2081
+ size: "sm",
2082
+ variant: tab === entry.id ? "primary" : "ghost",
2083
+ onClick: () => setTab(entry.id)
2084
+ },
2085
+ `${entry.label}${entry.count > 0 ? ` (${entry.count})` : ""}`
2086
+ )
2087
+ )
1228
2088
  ),
1229
- // 分组与刷新推到右边;配置页上没有它们。
1230
- isConfig ? null : React5.createElement(
2089
+ // 分组与刷新推到右边;只有三个**数据**页签上才有它们(配置页与磁盘占用页都没有
2090
+ // 可筛可选的列表,磁盘占用页自带一个只重拉 usage 的刷新按钮)。
2091
+ isDataTab ? React8.createElement(
1231
2092
  "div",
1232
2093
  { 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
- )
2094
+ React8.createElement(
2095
+ import_dsh_client_ui_primitives6.Button,
2096
+ {
2097
+ size: "sm",
2098
+ variant: "outline",
2099
+ title: t("groupHint"),
2100
+ onClick: () => {
2101
+ const next = !grouping;
2102
+ groupingStore.set(next);
2103
+ updateSettings({ grouping: next });
2104
+ }
2105
+ },
2106
+ grouping ? t("groupingOn") : t("groupingOff")
2107
+ ),
2108
+ React8.createElement(
2109
+ import_dsh_client_ui_primitives6.Button,
2110
+ { size: "sm", variant: "outline", disabled: loading || busy, onClick: () => void refresh() },
2111
+ t("refresh")
2112
+ )
2113
+ ) : null
1245
2114
  ),
1246
- error ? React5.createElement("div", { style: S.error }, error) : null,
1247
- isConfig ? null : React5.createElement(
2115
+ error ? React8.createElement("div", { style: S.error }, error) : null,
2116
+ // 过滤工具条:只有数据页签上有(配置页没有列表可筛,磁盘占用页也不是列表)。
2117
+ isDataTab ? filterBar : null,
2118
+ isDataTab ? React8.createElement(
1248
2119
  "div",
1249
2120
  { 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(
2121
+ loading && !snapshot ? React8.createElement("div", { style: S.empty }, t("loading")) : filtered.length === 0 ? React8.createElement(
2122
+ "div",
2123
+ { style: S.empty },
2124
+ filtering ? t("emptyFiltered") : tab === "archived" ? t("emptyArchived") : t("emptyPinned")
2125
+ ) : React8.createElement("div", { style: S.list }, nodes)
2126
+ ) : null,
2127
+ isDataTab ? React8.createElement(
1253
2128
  "div",
1254
2129
  { 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
- ),
2130
+ React8.createElement(
2131
+ import_dsh_client_ui_primitives6.Button,
2132
+ {
2133
+ size: "sm",
2134
+ variant: "ghost",
2135
+ disabled: busy || rows.length === 0,
2136
+ onClick: () => setSelected(selected.length === rows.length ? [] : rows.map((row) => row.sessionId))
2137
+ },
2138
+ selected.length === rows.length && rows.length > 0 ? t("selectNone") : t("selectAll")
2139
+ ),
2140
+ batch.map(
2141
+ (action) => React8.createElement(
2142
+ import_dsh_client_ui_primitives6.Button,
2143
+ {
2144
+ key: action.label,
2145
+ size: "sm",
2146
+ variant: action.variant || "ghost",
2147
+ disabled: busy || action.disabled === true,
2148
+ onClick: action.run
2149
+ },
2150
+ action.label
2151
+ )
2152
+ ),
2153
+ busy ? React8.createElement("span", { style: S.footer }, t("working")) : null
2154
+ ) : null,
2155
+ isUsage ? usagePage : null,
1276
2156
  isConfig ? configPage : null,
1277
- isConfig ? null : React5.createElement(
2157
+ isDataTab ? React8.createElement(
1278
2158
  "div",
1279
2159
  { 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
- )
2160
+ React8.createElement("span", null, text(t, "total", { n: rows.length, size: formatBytes(totalBytes) })),
2161
+ // 过滤只改显示:合计那一行报的仍是整个页签的条数,这里再补一句"筛出来几条"。
2162
+ filtering ? React8.createElement("span", null, text(t, "filtered", { n: filtered.length, total: rows.length })) : null,
2163
+ grouping && groups.length > 0 ? React8.createElement("span", null, text(t, "workspaces", { n: groups.length })) : null,
2164
+ selected.length > 0 ? React8.createElement("span", null, text(t, "selected", { n: selected.length })) : null,
2165
+ snapshot ? React8.createElement("span", { style: S.mono, title: snapshot.sessionsDir }, snapshot.sessionsDir) : null
2166
+ ) : null
2167
+ );
2168
+ }
2169
+
2170
+ // src/client/components/toast-host.ts
2171
+ var React9 = __toESM(require("react"), 1);
2172
+ var import_dsh_client_ui_primitives7 = require("@deepseek-ai/dsh-client-ui-primitives");
2173
+ var HOLD_MS = 5e3;
2174
+ function ToastHost(props) {
2175
+ const notice = useStore(toastStore);
2176
+ if (notice === null) return null;
2177
+ return React9.createElement(
2178
+ import_dsh_client_ui_primitives7.Toast,
2179
+ {
2180
+ text: notice.message,
2181
+ holdMs: HOLD_MS,
2182
+ onDone: dismissToast
2183
+ },
2184
+ `session-curator-toast-${notice.seq}`
1285
2185
  );
1286
2186
  }
1287
2187
 
2188
+ // src/client/components/delete-row-button.ts
2189
+ var React10 = __toESM(require("react"), 1);
2190
+ var import_dsh_client_ui_primitives8 = require("@deepseek-ai/dsh-client-ui-primitives");
2191
+ function DeleteRowButton(props) {
2192
+ const [hovered, setHovered] = React10.useState(false);
2193
+ const title = props.displayTitle || props.sessionId;
2194
+ const label = props.t("deleteSession");
2195
+ return React10.createElement(import_dsh_client_ui_primitives8.Tooltip, {
2196
+ label,
2197
+ side: "bottom",
2198
+ align: "end",
2199
+ delayMs: 500,
2200
+ children: React10.createElement(
2201
+ "button",
2202
+ {
2203
+ type: "button",
2204
+ style: hovered ? S.rowIconButtonHover : S.rowIconButton,
2205
+ "aria-label": `${title} — ${label}`,
2206
+ onPointerEnter: () => setHovered(true),
2207
+ onPointerLeave: () => setHovered(false),
2208
+ onClick: () => requestDelete([props.sessionId], title)
2209
+ },
2210
+ React10.createElement(import_dsh_client_ui_primitives8.IconTrashOutlineRegular, { size: 14 })
2211
+ )
2212
+ });
2213
+ }
2214
+
1288
2215
  // src/client/index.ts
1289
2216
  var MENU_ID = "session-curator.delete";
1290
2217
  var OVERLAY_ID = "session-curator.dialog";
2218
+ var TOAST_ID = "session-curator.toast";
2219
+ var HOVER_ID = "session-curator.hover";
2220
+ var ROW_ACTION_ID = "session-curator.delete-row";
1291
2221
  var SECTION_ID = "session-curator";
1292
2222
  var NAV_SECTION_ID = "archived-sessions";
1293
2223
  function sectionIdFor(ctx) {
@@ -1311,36 +2241,89 @@ function apply(ctx) {
1311
2241
  if (ctx.logger) ctx.logger.warn(`[session-curator] slot ${slot} 注册失败(${label})`);
1312
2242
  }
1313
2243
  };
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");
2244
+ contribute(
2245
+ "sidebar.workspaces.session.menu.item",
2246
+ {
2247
+ name: "sidebar.workspaces.session.menu.item",
2248
+ id: MENU_ID,
2249
+ order: 500,
2250
+ label: () => t("deleteSession"),
2251
+ locale: NS
2252
+ },
2253
+ (props) => {
2254
+ const [, setMenuOpen] = props.useMenuOpenState();
2255
+ return React11.createElement(
2256
+ import_dsh_client_ui_primitives9.MenuItemButton,
2257
+ {
2258
+ danger: true,
2259
+ separatorBefore: true,
2260
+ onSelect: () => {
2261
+ setMenuOpen(false);
2262
+ requestDelete([props.sessionId], props.displayTitle || props.sessionId);
2263
+ }
2264
+ },
2265
+ t("deleteSession")
2266
+ );
2267
+ },
2268
+ "menu item"
2269
+ );
2270
+ contribute(
2271
+ "shell.overlay",
2272
+ {
2273
+ name: "shell.overlay",
2274
+ id: OVERLAY_ID,
2275
+ order: 60,
2276
+ locale: NS
2277
+ },
2278
+ DialogHost,
2279
+ "dialog host"
2280
+ );
2281
+ contribute(
2282
+ "settings.section",
2283
+ {
2284
+ name: "settings.section",
2285
+ id: sectionIdFor(ctx),
2286
+ order: 45,
2287
+ label: () => t("sectionLabel"),
2288
+ locale: NS
2289
+ },
2290
+ SessionManagerPanel,
2291
+ "settings section"
2292
+ );
2293
+ contribute(
2294
+ "sidebar.session.row.hover",
2295
+ {
2296
+ name: "sidebar.session.row.hover",
2297
+ id: HOVER_ID,
2298
+ // 官方 ui-schedule 的任务列表在这个槽位是 order 10;我们排在它后面。
2299
+ order: 50,
2300
+ locale: NS
2301
+ },
2302
+ SessionHover,
2303
+ "hover card"
2304
+ );
2305
+ contribute(
2306
+ "sidebar.workspaces.session.row.action",
2307
+ {
2308
+ name: "sidebar.workspaces.session.row.action",
2309
+ id: ROW_ACTION_ID,
2310
+ order: 500,
2311
+ locale: NS
2312
+ },
2313
+ DeleteRowButton,
2314
+ "row action"
2315
+ );
2316
+ contribute(
2317
+ "shell.overlay",
2318
+ {
2319
+ name: "shell.overlay",
2320
+ id: TOAST_ID,
2321
+ order: 70,
2322
+ locale: NS
2323
+ },
2324
+ ToastHost,
2325
+ "toast host"
2326
+ );
1344
2327
  }
1345
2328
  })(module, module.exports, require);
1346
2329
  return module.exports;