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/README.md +2 -2
- package/README.zh-CN.md +1 -1
- package/lib/client.js +1645 -328
- package/lib/host/fs-utils.js +44 -3
- package/lib/host/open.js +104 -0
- package/lib/host/retention.js +24 -0
- package/lib/host/routes.js +22 -1
- package/lib/host/session-manager.js +267 -20
- package/lib/host/trash.js +99 -10
- package/lib/index.js +6 -2
- package/package.json +13 -10
- package/scripts/activation.mjs +0 -358
- package/scripts/build.mjs +0 -96
- package/scripts/client.mjs +0 -691
- package/scripts/smoke.mjs +0 -382
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
|
-
|
|
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
|
|
54
|
-
var
|
|
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
|
|
938
|
+
const purgeOld = request.kind === "purge-old";
|
|
939
|
+
const purge = request.kind === "purge" || purgeOld;
|
|
545
940
|
const many = request.ids.length > 1;
|
|
546
|
-
const
|
|
547
|
-
const
|
|
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
|
|
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(
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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/
|
|
609
|
-
var
|
|
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/
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
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
|
-
|
|
625
|
-
|
|
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
|
-
|
|
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(
|
|
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/
|
|
776
|
-
var
|
|
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
|
-
|
|
1428
|
+
const workspaceActions = props.workspaceActions ?? [];
|
|
1429
|
+
return React5.createElement(
|
|
785
1430
|
"div",
|
|
786
1431
|
{ style: props.first ? { ...S.groupHead, borderTop: "none" } : S.groupHead },
|
|
787
|
-
|
|
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
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
|
816
|
-
var
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1535
|
+
React6.createElement(
|
|
840
1536
|
"div",
|
|
841
1537
|
{ style: S.itemMain },
|
|
842
|
-
|
|
843
|
-
|
|
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
|
-
|
|
1547
|
+
React6.createElement(
|
|
846
1548
|
"div",
|
|
847
1549
|
{ style: S.itemActions },
|
|
848
|
-
|
|
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:
|
|
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:
|
|
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] =
|
|
879
|
-
const [snapshot, setSnapshot] =
|
|
880
|
-
const [error, setError] =
|
|
881
|
-
const [loading, setLoading] =
|
|
882
|
-
const [busy, setBusy] =
|
|
883
|
-
const [selected, setSelected] =
|
|
884
|
-
const [collapsedOverrides, setCollapsedOverrides] =
|
|
885
|
-
const [menuFor, setMenuFor] =
|
|
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] =
|
|
890
|
-
const [sidebarAtMount] =
|
|
1710
|
+
const [sidebarArchived, setSidebarArchived] = React8.useState(readSidebarArchived);
|
|
1711
|
+
const [sidebarAtMount] = React8.useState(readSidebarArchived);
|
|
891
1712
|
const applyResult = (value) => {
|
|
892
|
-
if (value && value.archived)
|
|
1713
|
+
if (value && value.archived) {
|
|
1714
|
+
rememberSnapshot(value);
|
|
1715
|
+
setSnapshot(value);
|
|
1716
|
+
}
|
|
893
1717
|
};
|
|
894
|
-
const refresh =
|
|
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
|
-
|
|
1729
|
+
React8.useEffect(() => {
|
|
906
1730
|
void refresh();
|
|
907
1731
|
}, [refresh, revision]);
|
|
908
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
969
|
-
{ id: "pin", label: t("pin"), icon:
|
|
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:
|
|
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
|
-
{
|
|
995
|
-
|
|
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({
|
|
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({
|
|
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
|
-
|
|
1015
|
-
batch.push({ label: t("
|
|
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
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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(
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
|
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
|
-
|
|
2033
|
+
React8.createElement(
|
|
1063
2034
|
"div",
|
|
1064
2035
|
{ style: S.configMain },
|
|
1065
|
-
|
|
1066
|
-
hint === null ? null :
|
|
2036
|
+
React8.createElement("span", { style: S.configLabel }, label),
|
|
2037
|
+
hint === null ? null : React8.createElement("span", { style: S.configHint }, hint)
|
|
1067
2038
|
),
|
|
1068
|
-
|
|
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" ?
|
|
1073
|
-
|
|
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
|
-
|
|
2106
|
+
React8.createElement(
|
|
1077
2107
|
"div",
|
|
1078
2108
|
{ style: S.card },
|
|
1079
|
-
configRow(
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
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 :
|
|
2168
|
+
sidebarArchived === sidebarAtMount ? null : React8.createElement(
|
|
1118
2169
|
"div",
|
|
1119
2170
|
{ key: "config:sidebar-refresh", style: S.configFoot },
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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
|
-
|
|
2183
|
+
},
|
|
2184
|
+
t("refreshPage")
|
|
2185
|
+
)
|
|
1131
2186
|
),
|
|
1132
|
-
configRow(
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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
|
-
|
|
2272
|
+
React8.createElement(
|
|
1167
2273
|
"div",
|
|
1168
2274
|
{ style: S.card },
|
|
1169
|
-
|
|
2275
|
+
React8.createElement(
|
|
1170
2276
|
"div",
|
|
1171
2277
|
{ style: { ...S.configRow, borderTop: "none" } },
|
|
1172
|
-
|
|
2278
|
+
React8.createElement(
|
|
1173
2279
|
"div",
|
|
1174
2280
|
{ style: S.configMain },
|
|
1175
|
-
|
|
1176
|
-
|
|
2281
|
+
React8.createElement("span", { style: S.configLabel }, t("aboutTitle")),
|
|
2282
|
+
React8.createElement("span", { style: S.configHint }, "dsh-session-curator")
|
|
1177
2283
|
),
|
|
1178
|
-
|
|
2284
|
+
React8.createElement(
|
|
1179
2285
|
"div",
|
|
1180
2286
|
{ style: S.configControl },
|
|
1181
2287
|
// 版本号:浅色小字。
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
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 :
|
|
1192
|
-
outdated && update.value !== null ?
|
|
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
|
-
|
|
1196
|
-
|
|
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
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
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
|
-
|
|
2331
|
+
const usagePage = React8.createElement(
|
|
1214
2332
|
"div",
|
|
1215
2333
|
{ style: S.column },
|
|
1216
|
-
|
|
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
|
-
|
|
2368
|
+
React8.createElement(
|
|
1220
2369
|
"div",
|
|
1221
2370
|
{ style: S.tabBar },
|
|
1222
|
-
tabs.map(
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
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
|
-
|
|
2384
|
+
// 分组与刷新推到右边;只有三个**数据**页签上才有它们(配置页与磁盘占用页都没有
|
|
2385
|
+
// 可筛可选的列表,磁盘占用页自带一个只重拉 usage 的刷新按钮)。
|
|
2386
|
+
isDataTab ? React8.createElement(
|
|
1231
2387
|
"div",
|
|
1232
2388
|
{ style: S.headerActions },
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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 ?
|
|
1247
|
-
|
|
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 ?
|
|
1251
|
-
|
|
1252
|
-
|
|
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
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
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
|
-
|
|
2465
|
+
isDataTab ? React8.createElement(
|
|
1278
2466
|
"div",
|
|
1279
2467
|
{ style: S.footer },
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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(
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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;
|