dsh-session-curator 0.1.1 → 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 +4 -11
- package/README.zh-CN.md +4 -7
- package/lib/client.js +2660 -1425
- package/lib/host/fs-utils.js +105 -0
- package/lib/host/manifest.js +22 -0
- package/lib/host/open.js +104 -0
- package/lib/host/paths.js +47 -0
- package/lib/host/retention.js +24 -0
- package/lib/host/routes.js +134 -0
- package/lib/host/session-manager.js +477 -0
- package/lib/host/trash.js +279 -0
- package/lib/host/update.js +67 -0
- package/lib/index.js +17 -678
- package/package.json +19 -9
- package/scripts/activation.mjs +0 -358
- package/scripts/client.mjs +0 -569
- package/scripts/smoke.mjs +0 -382
package/lib/client.js
CHANGED
|
@@ -1,1430 +1,2665 @@
|
|
|
1
|
-
|
|
2
|
-
* dsh-session-curator — client 半区(免构建手写束)。
|
|
3
|
-
*
|
|
4
|
-
* 三个贡献:
|
|
5
|
-
* 1. `sidebar.workspaces.session.menu.item`(order 500,排在官方 archive 之后):
|
|
6
|
-
* 一行「删除会话」,点击后弹确认框,确认即软删除到回收站。
|
|
7
|
-
* 2. `shell.overlay`:全局确认弹窗宿主(菜单项与设置面板共用)。
|
|
8
|
-
* 3. `settings.section`(order 45):会话管理面板 —— 已归档 / 已置顶 / 回收站。
|
|
9
|
-
*
|
|
10
|
-
* 与宿主通信走 Connection 的共享 `/api` 通道:官方调用器
|
|
11
|
-
* (`ctx.get('connection').rpc.call`,若该服务在客户端存在)或逐字复刻的
|
|
12
|
-
* `fetch` 兜底,目标都是 `/api/session-curator/<endpoint>`。信任栅栏与会话鉴权
|
|
13
|
-
* 仍由宿主 Connection 的 `/api` 前缀路由把关,插件自身无需任何鉴权代码。
|
|
14
|
-
*/
|
|
1
|
+
// 本文件由 `node scripts/build.mjs` 从 src/client/**/*.ts 生成,不要手改。
|
|
15
2
|
window.__ModuleLoader__.load({
|
|
16
3
|
id: "dsh-session-curator",
|
|
4
|
+
// 浏览器只认这种「工厂 + 局部 require」的束:factory 收到的 require 就是 DSH 的模块表,
|
|
5
|
+
// esbuild 把 external 的 import 编译成了对这个参数的调用。
|
|
17
6
|
factory: (require) => {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
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
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
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
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
7
|
+
const module = { exports: {} };
|
|
8
|
+
(function (module, exports, require) {
|
|
9
|
+
var __create = Object.create;
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
14
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
15
|
+
var __export = (target2, all) => {
|
|
16
|
+
for (var name in all)
|
|
17
|
+
__defProp(target2, name, { get: all[name], enumerable: true });
|
|
18
|
+
};
|
|
19
|
+
var __copyProps = (to, from, except, desc) => {
|
|
20
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
21
|
+
for (let key of __getOwnPropNames(from))
|
|
22
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
23
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
24
|
+
}
|
|
25
|
+
return to;
|
|
26
|
+
};
|
|
27
|
+
var __toESM = (mod, isNodeMode, target2) => (target2 = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
28
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
29
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
30
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
31
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
32
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target2, "default", { value: mod, enumerable: true }) : target2,
|
|
33
|
+
mod
|
|
34
|
+
));
|
|
35
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
36
|
+
|
|
37
|
+
// src/client/index.ts
|
|
38
|
+
var index_exports = {};
|
|
39
|
+
__export(index_exports, {
|
|
40
|
+
Dropdown: () => Dropdown,
|
|
41
|
+
GroupHeader: () => GroupHeader,
|
|
42
|
+
NS: () => NS,
|
|
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,
|
|
48
|
+
apply: () => apply,
|
|
49
|
+
cachedSnapshot: () => cachedSnapshot,
|
|
50
|
+
collapseKey: () => collapseKey,
|
|
51
|
+
describeSession: () => describeSession,
|
|
52
|
+
dialogStore: () => dialogStore,
|
|
53
|
+
effectiveTrashDays: () => effectiveTrashDays,
|
|
54
|
+
filterRows: () => filterRows,
|
|
55
|
+
forgetDetails: () => forgetDetails,
|
|
56
|
+
forgetSnapshot: () => forgetSnapshot,
|
|
57
|
+
groupKeyOf: () => groupKeyOf,
|
|
58
|
+
groupRows: () => groupRows,
|
|
59
|
+
inject: () => inject,
|
|
60
|
+
isFilterActive: () => isFilterActive,
|
|
61
|
+
isGroupCollapsed: () => isGroupCollapsed,
|
|
62
|
+
matchesFilter: () => matchesFilter,
|
|
63
|
+
matchesSearch: () => matchesSearch,
|
|
64
|
+
parseCustomDays: () => parseCustomDays,
|
|
65
|
+
sectionIdFor: () => sectionIdFor,
|
|
66
|
+
showNotice: () => showNotice,
|
|
67
|
+
showTrashUndo: () => showTrashUndo,
|
|
68
|
+
sortRows: () => sortRows,
|
|
69
|
+
toastStore: () => toastStore,
|
|
70
|
+
toggledCollapse: () => toggledCollapse,
|
|
71
|
+
undoTrash: () => undoTrash,
|
|
72
|
+
workspaceSummaries: () => workspaceSummaries
|
|
73
|
+
});
|
|
74
|
+
module.exports = __toCommonJS(index_exports);
|
|
75
|
+
var React11 = __toESM(require("react"), 1);
|
|
76
|
+
var import_dsh_client_ui_primitives9 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
77
|
+
|
|
78
|
+
// src/client/components/dialog-host.ts
|
|
79
|
+
var React2 = __toESM(require("react"), 1);
|
|
80
|
+
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
81
|
+
|
|
82
|
+
// src/client/runtime.ts
|
|
83
|
+
var current = null;
|
|
84
|
+
function setContext(ctx) {
|
|
85
|
+
current = ctx;
|
|
86
|
+
}
|
|
87
|
+
function connectionRpc() {
|
|
88
|
+
const ctx = current;
|
|
89
|
+
if (ctx === null) return null;
|
|
90
|
+
const connection = ctx.get("connection");
|
|
91
|
+
const rpc = connection === null || connection === void 0 ? null : connection.rpc;
|
|
92
|
+
return rpc && typeof rpc.call === "function" ? rpc : null;
|
|
93
|
+
}
|
|
94
|
+
function ctxLogger() {
|
|
95
|
+
const ctx = current;
|
|
96
|
+
return ctx !== null && ctx.logger ? ctx.logger : null;
|
|
97
|
+
}
|
|
98
|
+
function reportError(error) {
|
|
99
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
+
const logger = ctxLogger();
|
|
101
|
+
if (logger) logger.warn(`[session-curator] ${message}`);
|
|
102
|
+
return message;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/client/api.ts
|
|
106
|
+
var API_CHANNEL = "/api";
|
|
107
|
+
var ROUTE_PREFIX = "session-curator";
|
|
108
|
+
function randomUuid() {
|
|
109
|
+
const bytes = new Uint8Array(16);
|
|
110
|
+
const crypto = globalThis.crypto;
|
|
111
|
+
if (crypto && typeof crypto.getRandomValues === "function") crypto.getRandomValues(bytes);
|
|
112
|
+
else for (let index = 0; index < bytes.length; index += 1) bytes[index] = Math.floor(Math.random() * 256);
|
|
113
|
+
const view = new DataView(bytes.buffer);
|
|
114
|
+
view.setUint8(6, view.getUint8(6) & 15 | 64);
|
|
115
|
+
view.setUint8(8, view.getUint8(8) & 63 | 128);
|
|
116
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
117
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
118
|
+
}
|
|
119
|
+
function target(endpoint) {
|
|
120
|
+
return `${ROUTE_PREFIX}/${endpoint}`;
|
|
121
|
+
}
|
|
122
|
+
async function rawCall(endpoint, payload) {
|
|
123
|
+
const rpcId = randomUuid();
|
|
124
|
+
const url = `${API_CHANNEL}/${ROUTE_PREFIX}/${endpoint}`;
|
|
125
|
+
const response = await fetch(url.slice(1), {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: { "content-type": "application/json" },
|
|
128
|
+
body: JSON.stringify({ type: "client-request", rpcId, method: target(endpoint), payload: payload || {} })
|
|
129
|
+
});
|
|
130
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}(${url})`);
|
|
131
|
+
let envelope;
|
|
132
|
+
try {
|
|
133
|
+
envelope = await response.json();
|
|
134
|
+
} catch {
|
|
135
|
+
throw new Error(`响应不是 JSON(${url})`);
|
|
136
|
+
}
|
|
137
|
+
const message = envelope;
|
|
138
|
+
if (!message || message.type !== "server-response" || typeof message.rpcId !== "string") {
|
|
139
|
+
throw new Error("无效的 server-response 信封");
|
|
140
|
+
}
|
|
141
|
+
if (message.rpcId !== rpcId) throw new Error(`rpcId 不匹配(${endpoint})`);
|
|
142
|
+
const result = message.result;
|
|
143
|
+
if (!result || result.ok !== true && result.ok !== false) throw new Error("无效的 server-response 结果");
|
|
144
|
+
if (result.ok !== true) {
|
|
145
|
+
throw new Error(`${result.error ? result.error.code : "error"}: ${result.error ? result.error.message : "未知错误"}`);
|
|
146
|
+
}
|
|
147
|
+
return result.value;
|
|
148
|
+
}
|
|
149
|
+
async function call(endpoint, payload) {
|
|
150
|
+
const rpc = connectionRpc();
|
|
151
|
+
if (rpc !== null) {
|
|
152
|
+
const result = await rpc.call(API_CHANNEL, target(endpoint), payload || {});
|
|
153
|
+
if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
|
|
154
|
+
return result.value;
|
|
155
|
+
}
|
|
156
|
+
return await rawCall(endpoint, payload);
|
|
157
|
+
}
|
|
158
|
+
function firstFailure(results) {
|
|
159
|
+
for (const row of results || []) {
|
|
160
|
+
if (!row.ok) return row.reason || "操作失败";
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/client/stores.ts
|
|
166
|
+
var React = __toESM(require("react"), 1);
|
|
167
|
+
function createStore(initial) {
|
|
168
|
+
let value = initial;
|
|
169
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
170
|
+
return {
|
|
171
|
+
get: () => value,
|
|
172
|
+
set: (next) => {
|
|
173
|
+
value = next;
|
|
174
|
+
for (const listener of listeners) listener();
|
|
175
|
+
},
|
|
176
|
+
subscribe: (listener) => {
|
|
177
|
+
listeners.add(listener);
|
|
178
|
+
return () => {
|
|
179
|
+
listeners.delete(listener);
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function useStore(store) {
|
|
185
|
+
const [value, setValue] = React.useState(store.get);
|
|
186
|
+
React.useEffect(() => store.subscribe(() => setValue(store.get())), [store]);
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/client/dialog.ts
|
|
191
|
+
var dialogStore = createStore(null);
|
|
192
|
+
var mutationStore = createStore(0);
|
|
193
|
+
function requestTrash(ids, title) {
|
|
194
|
+
dialogStore.set({ kind: "trash", ids, title });
|
|
195
|
+
}
|
|
196
|
+
function requestPurge(ids, title) {
|
|
197
|
+
dialogStore.set({ kind: "purge", ids, title });
|
|
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
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/client/i18n.ts
|
|
230
|
+
var NS = "session-curator";
|
|
231
|
+
var zh = {
|
|
232
|
+
sectionLabel: "会话管理",
|
|
233
|
+
menuDelete: "删除会话",
|
|
234
|
+
titleArchived: "已归档",
|
|
235
|
+
titlePinned: "已置顶",
|
|
236
|
+
titleTrash: "回收站",
|
|
237
|
+
refresh: "刷新",
|
|
238
|
+
loading: "读取中…",
|
|
239
|
+
retry: "重试",
|
|
240
|
+
emptyArchived: "还没有归档的会话。在会话行的「⋯」菜单里归档后会出现在这里。",
|
|
241
|
+
emptyPinned: "没有置顶的会话。",
|
|
242
|
+
emptyTrash: "回收站是空的。",
|
|
243
|
+
unarchive: "取消归档",
|
|
244
|
+
pin: "置顶",
|
|
245
|
+
unpin: "取消置顶",
|
|
246
|
+
restore: "恢复",
|
|
247
|
+
restoreUnavailable: "无法恢复(缺元数据)",
|
|
248
|
+
purge: "永久删除",
|
|
249
|
+
moveToTrash: "移入回收站",
|
|
250
|
+
rowActions: "更多操作",
|
|
251
|
+
selectAll: "全选",
|
|
252
|
+
selectNone: "取消选择",
|
|
253
|
+
selected: "已选 {n} 项",
|
|
254
|
+
emptyTrashAll: "清空回收站",
|
|
255
|
+
total: "合计 {n} 个会话 · {size}",
|
|
256
|
+
working: "处理中…",
|
|
257
|
+
sessionRunning: "会话正在运行,先停止它再删除。",
|
|
258
|
+
confirmTrashTitle: "删除会话",
|
|
259
|
+
confirmTrashBody: "「{title}」会被移到回收站,之后可以在「设置 → 会话管理」里恢复。",
|
|
260
|
+
confirmTrashAction: "移到回收站",
|
|
261
|
+
confirmPurgeTitle: "永久删除",
|
|
262
|
+
confirmPurgeBody: "「{title}」将从回收站永久删除,日志文件会被彻底移除,无法恢复。",
|
|
263
|
+
confirmPurgeManyBody: "选中的 {n} 个会话将从回收站永久删除,无法恢复。",
|
|
264
|
+
confirmPurgeAction: "永久删除",
|
|
265
|
+
ackPurge: "我明白这无法撤销",
|
|
266
|
+
cancel: "取消",
|
|
267
|
+
close: "关闭",
|
|
268
|
+
noProject: "(未知目录)",
|
|
269
|
+
files: "{n} 个文件",
|
|
270
|
+
groupingOn: "分组:工作区",
|
|
271
|
+
groupingOff: "分组:平铺",
|
|
272
|
+
groupHint: "按会话原来的工作区文件夹归类;点分组标题折叠,左边的勾选框选中整个工作区",
|
|
273
|
+
sessions: "{n} 个会话",
|
|
274
|
+
workspaces: "{n} 个工作区",
|
|
275
|
+
selectGroup: "全选该工作区",
|
|
276
|
+
clearGroup: "取消选中该工作区",
|
|
277
|
+
selectAllShort: "全选",
|
|
278
|
+
clearShort: "取消",
|
|
279
|
+
collapse: "折叠",
|
|
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: "没有可统计的会话。",
|
|
311
|
+
titleConfig: "配置",
|
|
312
|
+
configTitle: "基础设置",
|
|
313
|
+
configTab: "默认页签",
|
|
314
|
+
configTabHint: "打开这里时先显示哪个页签",
|
|
315
|
+
configGrouping: "按工作区分组",
|
|
316
|
+
configGroupingHint: "关掉就是平铺列表;顶部的「分组」按钮改的是同一个值",
|
|
317
|
+
configGroupDefault: "分组默认状态",
|
|
318
|
+
configGroupDefaultHint: "以后打开的文件夹默认是折叠还是展开;手动点过的文件夹保留手动状态",
|
|
319
|
+
groupExpanded: "展开",
|
|
320
|
+
groupCollapsed: "折叠",
|
|
321
|
+
configSidebarArchived: "侧边栏显示已归档会话",
|
|
322
|
+
configSidebarArchivedHint: "对应侧边栏「视图选项」里的「隐藏已归档 / 全部对话(显示已归档)」;存的是官方那份设置,改完要刷新页面才生效",
|
|
323
|
+
configSidebarRefreshHint: "已写入官方设置,但侧边栏要刷新页面才会跟着变。",
|
|
324
|
+
refreshPage: "刷新页面",
|
|
325
|
+
configMeta: "行内显示体积与文件数",
|
|
326
|
+
configMetaHint: "关掉后每行只留标题(平铺时仍会显示文件夹)",
|
|
327
|
+
configOrder: "排序",
|
|
328
|
+
configOrderHint: "原顺序就是宿主给的顺序(回收站里是最近删除在前)",
|
|
329
|
+
orderHost: "原顺序",
|
|
330
|
+
orderTitle: "标题",
|
|
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: "我明白这无法撤销",
|
|
359
|
+
configAutoUpdate: "自动检查更新",
|
|
360
|
+
configAutoUpdateHint: "打开这个页面时问一次 npm registry(10 分钟内有缓存)",
|
|
361
|
+
configReset: "恢复默认",
|
|
362
|
+
configResetDone: "已恢复默认设置",
|
|
363
|
+
configSaved: "已保存",
|
|
364
|
+
aboutTitle: "关于",
|
|
365
|
+
versionLabel: "版本",
|
|
366
|
+
updateChecking: "正在检查更新…",
|
|
367
|
+
updateLatest: "已是最新版本",
|
|
368
|
+
updateAvailable: "有新版本 {latest}(当前 {current})",
|
|
369
|
+
updateFailed: "检查更新失败:{reason}",
|
|
370
|
+
updateRetry: "重新检查",
|
|
371
|
+
updateCheckNow: "检查更新",
|
|
372
|
+
updateStepsTitle: "升级步骤",
|
|
373
|
+
updateStepOne: "在终端里升级包(按你自己的 profile 名替换 desktop):",
|
|
374
|
+
updateStepTwo: "重启 DSH 让宿主半区生效;只改了客户端半区的话刷新页面就够。",
|
|
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}"
|
|
388
|
+
};
|
|
389
|
+
var en = {
|
|
390
|
+
sectionLabel: "Session manager",
|
|
391
|
+
menuDelete: "Delete session",
|
|
392
|
+
titleArchived: "Archived",
|
|
393
|
+
titlePinned: "Pinned",
|
|
394
|
+
titleTrash: "Trash",
|
|
395
|
+
refresh: "Refresh",
|
|
396
|
+
loading: "Loading…",
|
|
397
|
+
retry: "Retry",
|
|
398
|
+
emptyArchived: "No archived sessions yet. Archive one from its “⋯” menu and it shows up here.",
|
|
399
|
+
emptyPinned: "No pinned sessions.",
|
|
400
|
+
emptyTrash: "The trash is empty.",
|
|
401
|
+
unarchive: "Unarchive",
|
|
402
|
+
pin: "Pin",
|
|
403
|
+
unpin: "Unpin",
|
|
404
|
+
restore: "Restore",
|
|
405
|
+
restoreUnavailable: "Cannot restore (no metadata)",
|
|
406
|
+
purge: "Delete permanently",
|
|
407
|
+
moveToTrash: "Move to trash",
|
|
408
|
+
rowActions: "More actions",
|
|
409
|
+
selectAll: "Select all",
|
|
410
|
+
selectNone: "Clear selection",
|
|
411
|
+
selected: "{n} selected",
|
|
412
|
+
emptyTrashAll: "Empty trash",
|
|
413
|
+
total: "{n} sessions · {size}",
|
|
414
|
+
working: "Working…",
|
|
415
|
+
sessionRunning: "This session is running; stop it before deleting.",
|
|
416
|
+
confirmTrashTitle: "Delete session",
|
|
417
|
+
confirmTrashBody: "“{title}” moves to the trash. You can restore it later under Settings → Session manager.",
|
|
418
|
+
confirmTrashAction: "Move to trash",
|
|
419
|
+
confirmPurgeTitle: "Delete permanently",
|
|
420
|
+
confirmPurgeBody: "“{title}” is removed from the trash for good; its log files are gone and cannot be recovered.",
|
|
421
|
+
confirmPurgeManyBody: "{n} selected sessions are removed from the trash for good and cannot be recovered.",
|
|
422
|
+
confirmPurgeAction: "Delete permanently",
|
|
423
|
+
ackPurge: "I understand this cannot be undone",
|
|
424
|
+
cancel: "Cancel",
|
|
425
|
+
close: "Close",
|
|
426
|
+
noProject: "(unknown directory)",
|
|
427
|
+
files: "{n} files",
|
|
428
|
+
groupingOn: "Grouped: workspace",
|
|
429
|
+
groupingOff: "Grouped: off",
|
|
430
|
+
groupHint: "Group sessions under their original workspace folder; click a header to collapse it, the checkbox selects the whole workspace",
|
|
431
|
+
sessions: "{n} sessions",
|
|
432
|
+
workspaces: "{n} workspaces",
|
|
433
|
+
selectGroup: "Select this workspace",
|
|
434
|
+
clearGroup: "Clear this workspace",
|
|
435
|
+
selectAllShort: "Select all",
|
|
436
|
+
clearShort: "Clear",
|
|
437
|
+
collapse: "Collapse",
|
|
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.",
|
|
469
|
+
titleConfig: "Settings",
|
|
470
|
+
configTitle: "Basics",
|
|
471
|
+
configTab: "Default tab",
|
|
472
|
+
configTabHint: "Which tab the panel opens on",
|
|
473
|
+
configGrouping: "Group by workspace",
|
|
474
|
+
configGroupingHint: "Off renders a flat list; the top “Grouped” button edits the same value",
|
|
475
|
+
configGroupDefault: "Default folder state",
|
|
476
|
+
configGroupDefaultHint: "Whether folders start collapsed or expanded; folders you toggled keep their manual state",
|
|
477
|
+
groupExpanded: "Expanded",
|
|
478
|
+
groupCollapsed: "Collapsed",
|
|
479
|
+
configSidebarArchived: "Show archived sessions in the sidebar",
|
|
480
|
+
configSidebarArchivedHint: "Mirrors “Hide archived / All conversations” in the sidebar's view options; it is the official setting, so a page refresh applies it",
|
|
481
|
+
configSidebarRefreshHint: "Written to the official setting — the sidebar only follows it after a page refresh.",
|
|
482
|
+
refreshPage: "Refresh page",
|
|
483
|
+
configMeta: "Show size and file count per row",
|
|
484
|
+
configMetaHint: "Off leaves only the title (the folder still shows in the flat list)",
|
|
485
|
+
configOrder: "Order",
|
|
486
|
+
configOrderHint: "Host order is what the host sent (the trash is most-recently-deleted first)",
|
|
487
|
+
orderHost: "Host",
|
|
488
|
+
orderTitle: "Title",
|
|
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",
|
|
517
|
+
configAutoUpdate: "Check for updates automatically",
|
|
518
|
+
configAutoUpdateHint: "Asks the npm registry when this page opens (cached for 10 minutes)",
|
|
519
|
+
configReset: "Reset to defaults",
|
|
520
|
+
configResetDone: "Defaults restored",
|
|
521
|
+
configSaved: "Saved",
|
|
522
|
+
aboutTitle: "About",
|
|
523
|
+
versionLabel: "Version",
|
|
524
|
+
updateChecking: "Checking for updates…",
|
|
525
|
+
updateLatest: "You are on the latest version",
|
|
526
|
+
updateAvailable: "Version {latest} is available (you have {current})",
|
|
527
|
+
updateFailed: "Update check failed: {reason}",
|
|
528
|
+
updateRetry: "Check again",
|
|
529
|
+
updateCheckNow: "Check for updates",
|
|
530
|
+
updateStepsTitle: "How to upgrade",
|
|
531
|
+
updateStepOne: "Upgrade the package in a terminal (replace desktop with your profile name):",
|
|
532
|
+
updateStepTwo: "Restart DSH so the host half reloads; a client-half change only needs a page refresh.",
|
|
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}"
|
|
546
|
+
};
|
|
547
|
+
var dictionaries = { zh, en };
|
|
548
|
+
function text(t, key, params) {
|
|
549
|
+
let value = t(key);
|
|
550
|
+
if (params) {
|
|
551
|
+
for (const name of Object.keys(params)) value = value.split(`{${name}}`).join(String(params[name]));
|
|
552
|
+
}
|
|
553
|
+
return value;
|
|
554
|
+
}
|
|
555
|
+
var TAB_LABEL = {
|
|
556
|
+
archived: "titleArchived",
|
|
557
|
+
pinned: "titlePinned",
|
|
558
|
+
trash: "titleTrash"
|
|
559
|
+
};
|
|
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
|
+
|
|
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
|
+
};
|
|
588
|
+
var S = {
|
|
589
|
+
column: { display: "flex", flexDirection: "column", gap: "10px", padding: "4px 0", minWidth: 0 },
|
|
590
|
+
rowLine: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" },
|
|
591
|
+
tabBar: { display: "flex", alignItems: "center", gap: "6px" },
|
|
592
|
+
card: {
|
|
593
|
+
border: "1px solid var(--dsw-alias-border-l1)",
|
|
594
|
+
borderRadius: "10px",
|
|
595
|
+
overflow: "hidden"
|
|
596
|
+
},
|
|
597
|
+
list: { display: "flex", flexDirection: "column" },
|
|
598
|
+
item: {
|
|
599
|
+
display: "flex",
|
|
600
|
+
alignItems: "center",
|
|
601
|
+
gap: "10px",
|
|
602
|
+
padding: "8px 10px",
|
|
603
|
+
borderTop: "1px solid var(--dsw-alias-border-l1)"
|
|
604
|
+
},
|
|
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 },
|
|
613
|
+
itemTitle: {
|
|
614
|
+
color: "var(--dsw-alias-label-primary)",
|
|
615
|
+
fontSize: "13px",
|
|
616
|
+
overflow: "hidden",
|
|
617
|
+
textOverflow: "ellipsis",
|
|
618
|
+
whiteSpace: "nowrap",
|
|
619
|
+
minWidth: 0
|
|
620
|
+
},
|
|
621
|
+
itemMeta: {
|
|
622
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
623
|
+
fontSize: "11px",
|
|
624
|
+
overflow: "hidden",
|
|
625
|
+
textOverflow: "ellipsis",
|
|
626
|
+
whiteSpace: "nowrap"
|
|
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
|
+
},
|
|
657
|
+
itemActions: { display: "flex", alignItems: "center", gap: "6px", flex: "0 0 auto" },
|
|
658
|
+
/**
|
|
659
|
+
* 「⋯」触发器:图标按钮,弱化成次要前景色(跟侧边栏行的图标按钮一致)。
|
|
660
|
+
* `flex: 0 0 auto` 防"长标题挤坏按钮":行内标题再长也只能挤 `itemMain`
|
|
661
|
+
*(那边有 minWidth: 0 + 省略号),别让收缩落到这个按钮上。
|
|
662
|
+
*/
|
|
663
|
+
rowMenuButton: { color: "var(--dsw-alias-label-secondary)", padding: "0 6px", flex: "0 0 auto" },
|
|
664
|
+
/**
|
|
665
|
+
* 文件夹那一行的底色。**不能用 `--dsw-alias-bg-layer-*`**:浅色主题里
|
|
666
|
+
* base / layer-1 / layer-2 / layer-3 都指向同一个颜色(`neutral-bluish-00`),
|
|
667
|
+
* 铺上去等于没铺(这就是之前"看不出区别"的原因)。
|
|
668
|
+
* `interactive-bg-active` 是带 alpha 的叠加色(#2631481a,约 10%),
|
|
669
|
+
* 深浅主题都有效;比行 hover 用的 `interactive-bg-hover`(约 6%)略重一点,
|
|
670
|
+
* 这样"文件夹条"和"鼠标划过的会话行"不会糊在一起。
|
|
671
|
+
*/
|
|
672
|
+
groupHead: {
|
|
673
|
+
display: "flex",
|
|
674
|
+
alignItems: "center",
|
|
675
|
+
gap: "10px",
|
|
676
|
+
padding: "6px 10px",
|
|
677
|
+
background: "var(--dsw-alias-interactive-bg-active)",
|
|
678
|
+
borderTop: "1px solid var(--dsw-alias-border-l1)"
|
|
679
|
+
},
|
|
680
|
+
groupToggle: {
|
|
681
|
+
display: "flex",
|
|
682
|
+
alignItems: "center",
|
|
683
|
+
gap: "8px",
|
|
684
|
+
minWidth: 0,
|
|
685
|
+
flex: "1 1 auto",
|
|
686
|
+
background: "transparent",
|
|
687
|
+
border: "none",
|
|
688
|
+
padding: 0,
|
|
689
|
+
margin: 0,
|
|
690
|
+
color: "inherit",
|
|
691
|
+
font: "inherit",
|
|
692
|
+
textAlign: "left",
|
|
693
|
+
cursor: "pointer"
|
|
694
|
+
},
|
|
695
|
+
groupCaret: { color: "var(--dsw-alias-label-tertiary)", flex: "0 0 auto", display: "flex", alignItems: "center" },
|
|
696
|
+
groupIcon: { color: "var(--dsw-alias-label-secondary)", flex: "0 0 auto", display: "flex", alignItems: "center" },
|
|
697
|
+
groupTitle: {
|
|
698
|
+
color: "var(--dsw-alias-label-primary)",
|
|
699
|
+
fontSize: "12px",
|
|
700
|
+
fontWeight: 600,
|
|
701
|
+
overflow: "hidden",
|
|
702
|
+
textOverflow: "ellipsis",
|
|
703
|
+
whiteSpace: "nowrap",
|
|
704
|
+
flex: "0 1 auto"
|
|
705
|
+
},
|
|
706
|
+
groupMeta: { color: "var(--dsw-alias-label-tertiary)", fontSize: "11px", flex: "0 0 auto" },
|
|
707
|
+
/**
|
|
708
|
+
* 分组标题右侧的「全选 / 取消」。
|
|
709
|
+
*
|
|
710
|
+
* 为什么必须写这两条:路径一长,`groupHead` 这行必然溢出,浏览器按 flex 基准**按比例**
|
|
711
|
+
* 分配收缩量 —— 右侧按钮也会被挤窄。而 DSH 的 `Button` 是 `display: inline-flex`
|
|
712
|
+
* 且**没有 `white-space: nowrap`**,窄到一定程度「全选」两个字就在按钮里折成两行。
|
|
713
|
+
* 让它完全不参与收缩(`0 0 auto`)、再兜一条 `nowrap`,收缩就只剩左边的折叠按钮承担:
|
|
714
|
+
* 标题 / 路径各自 `minWidth: 0` + 省略号,该截断的地方截断。
|
|
715
|
+
*/
|
|
716
|
+
groupAction: { flex: "0 0 auto", whiteSpace: "nowrap" },
|
|
717
|
+
groupPath: {
|
|
718
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
719
|
+
fontSize: "11px",
|
|
720
|
+
overflow: "hidden",
|
|
721
|
+
textOverflow: "ellipsis",
|
|
722
|
+
whiteSpace: "nowrap",
|
|
723
|
+
minWidth: 0,
|
|
724
|
+
flex: "0 1 auto"
|
|
725
|
+
},
|
|
726
|
+
empty: { color: "var(--dsw-alias-label-secondary)", fontSize: "12px", padding: "14px 10px", textAlign: "center" },
|
|
727
|
+
error: { color: "var(--dsw-alias-state-error-primary)", fontSize: "12px" },
|
|
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 },
|
|
771
|
+
mono: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" },
|
|
772
|
+
ack: { display: "flex", alignItems: "center", gap: "8px", fontSize: "12px", color: "var(--dsw-alias-label-primary)" },
|
|
773
|
+
/** 顶部条:页签在左,动作靠 `marginInlineStart: auto` 推到右边。 */
|
|
774
|
+
header: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" },
|
|
775
|
+
headerActions: { display: "flex", alignItems: "center", gap: "8px", marginInlineStart: "auto" },
|
|
776
|
+
/** 配置页的一行:左边标签 + 说明,右边控件。 */
|
|
777
|
+
configRow: {
|
|
778
|
+
display: "flex",
|
|
779
|
+
alignItems: "center",
|
|
780
|
+
justifyContent: "space-between",
|
|
781
|
+
gap: "12px",
|
|
782
|
+
padding: "10px 12px",
|
|
783
|
+
borderTop: "1px solid var(--dsw-alias-border-l1)"
|
|
784
|
+
},
|
|
785
|
+
configMain: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0 },
|
|
786
|
+
configLabel: { color: "var(--dsw-alias-label-primary)", fontSize: "13px" },
|
|
787
|
+
configHint: { color: "var(--dsw-alias-label-tertiary)", fontSize: "11px" },
|
|
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)" },
|
|
797
|
+
configFoot: {
|
|
798
|
+
display: "flex",
|
|
799
|
+
alignItems: "center",
|
|
800
|
+
gap: "8px",
|
|
801
|
+
flexWrap: "wrap",
|
|
802
|
+
padding: "10px 12px",
|
|
803
|
+
borderTop: "1px solid var(--dsw-alias-border-l1)"
|
|
804
|
+
},
|
|
805
|
+
/** 版本号:浅色小字。 */
|
|
806
|
+
version: { color: "var(--dsw-alias-label-tertiary)", fontSize: "11px" },
|
|
807
|
+
/**
|
|
808
|
+
* 升级提示块。底色用带 alpha 的 `interactive-bg-active` —— 浅色主题里
|
|
809
|
+
* base / layer-1 / layer-2 / layer-3 是同一个颜色,铺上去等于没铺。
|
|
810
|
+
*/
|
|
811
|
+
updateBox: {
|
|
812
|
+
display: "flex",
|
|
813
|
+
flexDirection: "column",
|
|
814
|
+
gap: "6px",
|
|
815
|
+
padding: "10px 12px",
|
|
816
|
+
borderTop: "1px solid var(--dsw-alias-border-l1)",
|
|
817
|
+
background: "var(--dsw-alias-interactive-bg-active)"
|
|
818
|
+
},
|
|
819
|
+
updateTitle: { color: "var(--dsw-alias-label-primary)", fontSize: "12px", fontWeight: 600 },
|
|
820
|
+
updateSteps: { color: "var(--dsw-alias-label-secondary)", fontSize: "11px", display: "flex", flexDirection: "column", gap: "4px" },
|
|
821
|
+
code: {
|
|
822
|
+
display: "block",
|
|
823
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
824
|
+
fontSize: "11px",
|
|
825
|
+
color: "var(--dsw-alias-label-primary)",
|
|
826
|
+
background: "var(--dsw-alias-interactive-bg-active)",
|
|
827
|
+
borderRadius: "6px",
|
|
828
|
+
padding: "6px 8px",
|
|
829
|
+
overflowX: "auto",
|
|
830
|
+
whiteSpace: "pre"
|
|
831
|
+
},
|
|
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)" }
|
|
884
|
+
};
|
|
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
|
+
|
|
925
|
+
// src/client/components/dialog-host.ts
|
|
926
|
+
function DialogHost(props) {
|
|
927
|
+
const t = props.t;
|
|
928
|
+
const request = useStore(dialogStore);
|
|
929
|
+
const [busy, setBusy] = React2.useState(false);
|
|
930
|
+
const [ack, setAck] = React2.useState(false);
|
|
931
|
+
const [error, setError] = React2.useState(null);
|
|
932
|
+
const requestKey = request === null ? "" : `${request.kind}:${request.ids.join(",")}:${request.title}:${request.days ?? ""}`;
|
|
933
|
+
React2.useEffect(() => {
|
|
934
|
+
setAck(false);
|
|
935
|
+
setError(null);
|
|
936
|
+
}, [requestKey]);
|
|
937
|
+
if (request === null) return null;
|
|
938
|
+
const purgeOld = request.kind === "purge-old";
|
|
939
|
+
const purge = request.kind === "purge" || purgeOld;
|
|
940
|
+
const many = request.ids.length > 1;
|
|
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 });
|
|
944
|
+
const close = () => {
|
|
945
|
+
if (busy) return;
|
|
946
|
+
dialogStore.set(null);
|
|
947
|
+
};
|
|
948
|
+
const confirm = async () => {
|
|
949
|
+
setBusy(true);
|
|
950
|
+
try {
|
|
951
|
+
const endpoint = purgeOld ? "purge-older-than" : purge ? "purge" : "trash";
|
|
952
|
+
const value = await call(endpoint, purgeOld ? { days } : { ids: request.ids });
|
|
953
|
+
const failure = firstFailure(value.results);
|
|
954
|
+
if (failure === null) {
|
|
955
|
+
dialogStore.set(null);
|
|
956
|
+
rememberSnapshot(value);
|
|
957
|
+
mutationStore.set(mutationStore.get() + 1);
|
|
958
|
+
if (!purge) showTrashUndo(request.ids, request.title);
|
|
959
|
+
} else {
|
|
960
|
+
setError(failure);
|
|
961
|
+
}
|
|
962
|
+
} catch (thrown) {
|
|
963
|
+
setError(reportError(thrown));
|
|
964
|
+
} finally {
|
|
965
|
+
setBusy(false);
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
return React2.createElement(
|
|
969
|
+
import_dsh_client_ui_primitives.Modal,
|
|
970
|
+
{
|
|
971
|
+
open: true,
|
|
972
|
+
onClose: close,
|
|
973
|
+
title,
|
|
974
|
+
closeLabel: t("close"),
|
|
975
|
+
description: body,
|
|
976
|
+
footer: React2.createElement(
|
|
977
|
+
"div",
|
|
978
|
+
{ style: S.rowLine },
|
|
979
|
+
React2.createElement(import_dsh_client_ui_primitives.Button, { size: "md", variant: "ghost", disabled: busy, onClick: close }, t("cancel")),
|
|
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
|
+
)
|
|
990
|
+
)
|
|
991
|
+
},
|
|
992
|
+
purge ? React2.createElement(
|
|
993
|
+
"div",
|
|
994
|
+
{ style: S.column },
|
|
995
|
+
React2.createElement(
|
|
996
|
+
"label",
|
|
997
|
+
{ style: S.ack },
|
|
998
|
+
React2.createElement("input", {
|
|
999
|
+
type: "checkbox",
|
|
1000
|
+
checked: ack,
|
|
1001
|
+
disabled: busy,
|
|
1002
|
+
onChange: (event) => setAck(Boolean(event.target.checked))
|
|
1003
|
+
}),
|
|
1004
|
+
purgeOld ? t("ackPurgeOld") : t("ackPurge")
|
|
1005
|
+
),
|
|
1006
|
+
error ? React2.createElement("div", { style: S.error }, error) : null
|
|
1007
|
+
) : error ? React2.createElement("div", { style: S.error }, error) : null
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// src/client/components/session-hover.ts
|
|
1012
|
+
var React3 = __toESM(require("react"), 1);
|
|
1013
|
+
|
|
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
|
+
);
|
|
1081
|
+
}
|
|
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);
|
|
1111
|
+
}
|
|
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
|
+
}
|
|
1130
|
+
try {
|
|
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;
|
|
1143
|
+
} catch {
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
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
|
+
}
|
|
1170
|
+
function groupKeyOf(row) {
|
|
1171
|
+
if (typeof row.cwd === "string" && row.cwd.trim() !== "") {
|
|
1172
|
+
return `cwd:${row.cwd.trim().replace(/[\\/]+$/, "").replace(/\\/g, "/").toLowerCase()}`;
|
|
1173
|
+
}
|
|
1174
|
+
if (typeof row.workspaceId === "string" && row.workspaceId !== "") return `id:${row.workspaceId}`;
|
|
1175
|
+
return "unknown";
|
|
1176
|
+
}
|
|
1177
|
+
function groupRows(rows, t) {
|
|
1178
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1179
|
+
for (const row of rows) {
|
|
1180
|
+
const key = groupKeyOf(row);
|
|
1181
|
+
let group = groups.get(key);
|
|
1182
|
+
if (group === void 0) {
|
|
1183
|
+
const cwd = typeof row.cwd === "string" && row.cwd !== "" ? row.cwd : null;
|
|
1184
|
+
const title = (typeof row.workspaceTitle === "string" && row.workspaceTitle !== "" ? row.workspaceTitle : null) ?? (cwd === null ? null : baseName(cwd) || cwd) ?? t("noProject");
|
|
1185
|
+
group = { key, title, cwd, bytes: 0, rows: [] };
|
|
1186
|
+
groups.set(key, group);
|
|
1187
|
+
}
|
|
1188
|
+
group.rows.push(row);
|
|
1189
|
+
if (typeof row.bytes === "number" && Number.isFinite(row.bytes)) group.bytes += row.bytes;
|
|
1190
|
+
}
|
|
1191
|
+
const list = [...groups.values()];
|
|
1192
|
+
list.sort((left, right) => {
|
|
1193
|
+
if (left.key === "unknown" || right.key === "unknown") {
|
|
1194
|
+
return left.key === right.key ? 0 : left.key === "unknown" ? 1 : -1;
|
|
1195
|
+
}
|
|
1196
|
+
return left.title.localeCompare(right.title);
|
|
1197
|
+
});
|
|
1198
|
+
return list;
|
|
1199
|
+
}
|
|
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
|
+
};
|
|
1211
|
+
if (order === "title") {
|
|
1212
|
+
return [...rows].sort((left, right) => String(left.title ?? "").localeCompare(String(right.title ?? "")));
|
|
1213
|
+
}
|
|
1214
|
+
if (order === "bytes") {
|
|
1215
|
+
return [...rows].sort((left, right) => (right.bytes ?? 0) - (left.bytes ?? 0));
|
|
1216
|
+
}
|
|
1217
|
+
if (order === "createdAt") return [...rows].sort(descending((row) => row.createdAt));
|
|
1218
|
+
if (order === "deletedAt") return [...rows].sort(descending((row) => row.deletedAt));
|
|
1219
|
+
return rows;
|
|
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
|
+
}
|
|
1250
|
+
function collapseKey(tab, groupKey) {
|
|
1251
|
+
return `${tab}:${groupKey}`;
|
|
1252
|
+
}
|
|
1253
|
+
function isGroupCollapsed(key, overrides, fallback) {
|
|
1254
|
+
const manual = overrides[key];
|
|
1255
|
+
return manual === void 0 ? fallback === "collapsed" : manual;
|
|
1256
|
+
}
|
|
1257
|
+
function toggledCollapse(key, overrides, fallback) {
|
|
1258
|
+
const next = { ...overrides };
|
|
1259
|
+
const value = !isGroupCollapsed(key, overrides, fallback);
|
|
1260
|
+
if (value === (fallback === "collapsed")) delete next[key];
|
|
1261
|
+
else next[key] = value;
|
|
1262
|
+
return next;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
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
|
+
}
|
|
1281
|
+
var DEFAULT_SETTINGS = {
|
|
1282
|
+
tab: "archived",
|
|
1283
|
+
grouping: true,
|
|
1284
|
+
// 与 0.1.1 的行为一致:分组默认是**展开**的,老用户升级后界面不变。
|
|
1285
|
+
groupDefault: "expanded",
|
|
1286
|
+
showMeta: true,
|
|
1287
|
+
order: "host",
|
|
1288
|
+
autoUpdate: true,
|
|
1289
|
+
search: "",
|
|
1290
|
+
timeFilter: "all",
|
|
1291
|
+
trashDays: DEFAULT_TRASH_DAYS,
|
|
1292
|
+
trashDaysCustom: ""
|
|
1293
|
+
};
|
|
1294
|
+
var SEARCH_MAX_LENGTH = 200;
|
|
1295
|
+
var SETTINGS_KEY = "dsh-session-curator.settings";
|
|
1296
|
+
function readSettings() {
|
|
1297
|
+
try {
|
|
1298
|
+
const raw = globalThis.localStorage ? globalThis.localStorage.getItem(SETTINGS_KEY) : null;
|
|
1299
|
+
if (typeof raw !== "string" || raw === "") return { ...DEFAULT_SETTINGS };
|
|
1300
|
+
const parsed = JSON.parse(raw);
|
|
1301
|
+
if (parsed === null || typeof parsed !== "object") return { ...DEFAULT_SETTINGS };
|
|
1302
|
+
const next = { ...DEFAULT_SETTINGS };
|
|
1303
|
+
const pick = (key, validate) => {
|
|
1304
|
+
const value = validate(parsed[key]);
|
|
1305
|
+
if (value !== void 0) next[key] = value;
|
|
1306
|
+
};
|
|
1307
|
+
pick("tab", (value) => value === "archived" || value === "pinned" || value === "trash" ? value : void 0);
|
|
1308
|
+
pick("grouping", (value) => typeof value === "boolean" ? value : void 0);
|
|
1309
|
+
pick("groupDefault", (value) => value === "expanded" || value === "collapsed" ? value : void 0);
|
|
1310
|
+
pick("showMeta", (value) => typeof value === "boolean" ? value : void 0);
|
|
1311
|
+
pick(
|
|
1312
|
+
"order",
|
|
1313
|
+
(value) => value === "host" || value === "title" || value === "bytes" || value === "createdAt" || value === "deletedAt" ? value : void 0
|
|
1314
|
+
);
|
|
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);
|
|
1320
|
+
return next;
|
|
1321
|
+
} catch {
|
|
1322
|
+
return { ...DEFAULT_SETTINGS };
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
var settingsStore = createStore(readSettings());
|
|
1326
|
+
function updateSettings(patch) {
|
|
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);
|
|
1329
|
+
settingsStore.set(next);
|
|
1330
|
+
try {
|
|
1331
|
+
if (globalThis.localStorage) globalThis.localStorage.setItem(SETTINGS_KEY, JSON.stringify(next));
|
|
1332
|
+
} catch {
|
|
1333
|
+
}
|
|
1334
|
+
return next;
|
|
1335
|
+
}
|
|
1336
|
+
var groupingStore = createStore(settingsStore.get().grouping);
|
|
1337
|
+
var SIDEBAR_VIEW_KEY = "dsh.workspace.view.v5";
|
|
1338
|
+
function readSidebarArchived() {
|
|
1339
|
+
try {
|
|
1340
|
+
const raw = globalThis.localStorage ? globalThis.localStorage.getItem(SIDEBAR_VIEW_KEY) : null;
|
|
1341
|
+
if (typeof raw !== "string" || raw === "") return "default";
|
|
1342
|
+
const parsed = JSON.parse(raw);
|
|
1343
|
+
const value = parsed !== null && typeof parsed === "object" ? parsed.archivedFilter : null;
|
|
1344
|
+
return value === "show" || value === "only" ? value : "default";
|
|
1345
|
+
} catch {
|
|
1346
|
+
return "default";
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
function writeSidebarArchived(show) {
|
|
1350
|
+
try {
|
|
1351
|
+
if (!globalThis.localStorage) return false;
|
|
1352
|
+
const raw = globalThis.localStorage.getItem(SIDEBAR_VIEW_KEY);
|
|
1353
|
+
const parsed = raw === null || raw === "" ? null : JSON.parse(raw);
|
|
1354
|
+
const target2 = parsed === null || typeof parsed !== "object" || Array.isArray(parsed) ? {} : parsed;
|
|
1355
|
+
target2.archivedFilter = show ? "show" : "default";
|
|
1356
|
+
globalThis.localStorage.setItem(SIDEBAR_VIEW_KEY, JSON.stringify(target2));
|
|
1357
|
+
return true;
|
|
1358
|
+
} catch {
|
|
1359
|
+
return false;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// src/client/update.ts
|
|
1364
|
+
var updateStore = createStore({ state: "idle", value: null, error: null });
|
|
1365
|
+
async function checkUpdate(force) {
|
|
1366
|
+
updateStore.set({ state: "checking", value: updateStore.get().value, error: null });
|
|
1367
|
+
try {
|
|
1368
|
+
const value = await call("check-update", force === true ? { force: true } : {});
|
|
1369
|
+
updateStore.set({ state: "done", value, error: null });
|
|
1370
|
+
} catch (error) {
|
|
1371
|
+
updateStore.set({ state: "failed", value: updateStore.get().value, error: reportError(error) });
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// src/client/components/dropdown.ts
|
|
1376
|
+
var React4 = __toESM(require("react"), 1);
|
|
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");
|
|
1422
|
+
function GroupHeader(props) {
|
|
1423
|
+
const group = props.group;
|
|
1424
|
+
const t = props.t;
|
|
1425
|
+
const total = group.rows.length;
|
|
1426
|
+
const allSelected = total > 0 && group.rows.every((row) => props.selected.includes(row.sessionId));
|
|
1427
|
+
const action = allSelected ? t("clearShort") : t("selectAllShort");
|
|
1428
|
+
const workspaceActions = props.workspaceActions ?? [];
|
|
1429
|
+
return React5.createElement(
|
|
1430
|
+
"div",
|
|
1431
|
+
{ style: props.first ? { ...S.groupHead, borderTop: "none" } : S.groupHead },
|
|
1432
|
+
React5.createElement(
|
|
1433
|
+
"button",
|
|
1434
|
+
{
|
|
1435
|
+
type: "button",
|
|
1436
|
+
style: S.groupToggle,
|
|
1437
|
+
title: props.collapsed ? t("expand") : t("collapse"),
|
|
1438
|
+
onClick: () => props.onToggleCollapse()
|
|
1439
|
+
},
|
|
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
|
|
1454
|
+
),
|
|
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
|
+
})
|
|
1488
|
+
);
|
|
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
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// src/client/components/session-row.ts
|
|
1510
|
+
var React6 = __toESM(require("react"), 1);
|
|
1511
|
+
var import_dsh_client_ui_primitives4 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
1512
|
+
function SessionRow(props) {
|
|
1513
|
+
const row = props.row;
|
|
1514
|
+
const t = props.t;
|
|
1515
|
+
const meta = [];
|
|
1516
|
+
if (props.showCwd) meta.push(row.cwd ? row.cwd : t("noProject"));
|
|
1517
|
+
if (props.showMeta !== false) {
|
|
1518
|
+
if (row.bytes) meta.push(formatBytes(row.bytes));
|
|
1519
|
+
if (row.files) meta.push(text(t, "files", { n: row.files }));
|
|
1520
|
+
}
|
|
1521
|
+
const time = props.showDeletedAt ? formatTime(row.deletedAt) : formatTime(row.createdAt);
|
|
1522
|
+
if (time !== "") meta.push(time);
|
|
1523
|
+
const menu = props.menu;
|
|
1524
|
+
const running = row.active === true;
|
|
1525
|
+
return React6.createElement(
|
|
1526
|
+
"div",
|
|
1527
|
+
{ style: props.first ? { ...S.item, borderTop: "none" } : S.item },
|
|
1528
|
+
React6.createElement("input", {
|
|
1529
|
+
type: "checkbox",
|
|
1530
|
+
checked: props.checked,
|
|
1531
|
+
disabled: props.busy,
|
|
1532
|
+
"aria-label": row.title,
|
|
1533
|
+
onChange: () => props.onToggle()
|
|
1534
|
+
}),
|
|
1535
|
+
React6.createElement(
|
|
1536
|
+
"div",
|
|
1537
|
+
{ style: S.itemMain },
|
|
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(" · "))
|
|
1546
|
+
),
|
|
1547
|
+
React6.createElement(
|
|
1548
|
+
"div",
|
|
1549
|
+
{ style: S.itemActions },
|
|
1550
|
+
React6.createElement(import_dsh_client_ui_primitives4.Menu, {
|
|
1551
|
+
open: props.menuOpen,
|
|
1552
|
+
portal: true,
|
|
1553
|
+
side: "bottom",
|
|
1554
|
+
align: "end",
|
|
1555
|
+
closeOnPointerLeave: true,
|
|
1556
|
+
items: menu.items,
|
|
1557
|
+
onSelect: (id) => menu.run(id),
|
|
1558
|
+
onClose: () => props.onCloseMenu(),
|
|
1559
|
+
anchor: React6.createElement(import_dsh_client_ui_primitives4.Button, {
|
|
1560
|
+
size: "sm",
|
|
1561
|
+
variant: "ghost",
|
|
1562
|
+
disabled: props.busy,
|
|
1563
|
+
style: S.rowMenuButton,
|
|
1564
|
+
icon: React6.createElement(import_dsh_client_ui_primitives4.IconEllipsisOutlineRegular, {}),
|
|
1565
|
+
"aria-label": `${row.title} — ${t("rowActions")}`,
|
|
1566
|
+
onClick: (event) => {
|
|
1567
|
+
event.stopPropagation();
|
|
1568
|
+
props.onToggleMenu();
|
|
1569
|
+
}
|
|
1570
|
+
})
|
|
1571
|
+
})
|
|
1572
|
+
)
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
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
|
+
|
|
1686
|
+
// src/client/components/settings-panel.ts
|
|
1687
|
+
function SessionManagerPanel(props) {
|
|
1688
|
+
const t = props.t;
|
|
1689
|
+
const settings = useStore(settingsStore);
|
|
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);
|
|
1707
|
+
const grouping = useStore(groupingStore);
|
|
1708
|
+
const revision = useStore(mutationStore);
|
|
1709
|
+
const update = useStore(updateStore);
|
|
1710
|
+
const [sidebarArchived, setSidebarArchived] = React8.useState(readSidebarArchived);
|
|
1711
|
+
const [sidebarAtMount] = React8.useState(readSidebarArchived);
|
|
1712
|
+
const applyResult = (value) => {
|
|
1713
|
+
if (value && value.archived) {
|
|
1714
|
+
rememberSnapshot(value);
|
|
1715
|
+
setSnapshot(value);
|
|
1716
|
+
}
|
|
1717
|
+
};
|
|
1718
|
+
const refresh = React8.useCallback(async () => {
|
|
1719
|
+
setLoading(true);
|
|
1720
|
+
try {
|
|
1721
|
+
applyResult(await call("snapshot", {}));
|
|
1722
|
+
setError(null);
|
|
1723
|
+
} catch (failure) {
|
|
1724
|
+
setError(reportError(failure));
|
|
1725
|
+
} finally {
|
|
1726
|
+
setLoading(false);
|
|
1727
|
+
}
|
|
1728
|
+
}, []);
|
|
1729
|
+
React8.useEffect(() => {
|
|
1730
|
+
void refresh();
|
|
1731
|
+
}, [refresh, revision]);
|
|
1732
|
+
React8.useEffect(() => {
|
|
1733
|
+
if (tab !== "config" || !settings.autoUpdate || update.state !== "idle") return;
|
|
1734
|
+
void checkUpdate(false);
|
|
1735
|
+
}, [tab, settings.autoUpdate, update.state]);
|
|
1736
|
+
React8.useEffect(() => {
|
|
1737
|
+
setSelected([]);
|
|
1738
|
+
setMenuFor(null);
|
|
1739
|
+
setWorkspaceMenuFor(null);
|
|
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
|
+
};
|
|
1784
|
+
const run = async (endpoint, ids) => {
|
|
1785
|
+
if (ids.length === 0) return;
|
|
1786
|
+
setBusy(true);
|
|
1787
|
+
try {
|
|
1788
|
+
const value = await call(endpoint, { ids });
|
|
1789
|
+
applyResult(value);
|
|
1790
|
+
setError(firstFailure(value.results));
|
|
1791
|
+
setSelected([]);
|
|
1792
|
+
} catch (failure) {
|
|
1793
|
+
setError(reportError(failure));
|
|
1794
|
+
} finally {
|
|
1795
|
+
setBusy(false);
|
|
1796
|
+
}
|
|
1797
|
+
};
|
|
1798
|
+
const toggle = (sessionId) => {
|
|
1799
|
+
setSelected((current2) => current2.includes(sessionId) ? current2.filter((id) => id !== sessionId) : current2.concat([sessionId]));
|
|
1800
|
+
};
|
|
1801
|
+
const toggleGroup = (group) => {
|
|
1802
|
+
const ids = group.rows.map((row) => row.sessionId);
|
|
1803
|
+
const all = ids.every((id) => selected.includes(id));
|
|
1804
|
+
setSelected((current2) => all ? current2.filter((id) => !ids.includes(id)) : [...new Set(current2.concat(ids))]);
|
|
1805
|
+
};
|
|
1806
|
+
const toggleCollapse = (key) => {
|
|
1807
|
+
setCollapsedOverrides((current2) => toggledCollapse(key, current2, settings.groupDefault));
|
|
1808
|
+
};
|
|
1809
|
+
const archived = snapshot ? snapshot.archived : [];
|
|
1810
|
+
const pinned = snapshot ? snapshot.pinned : [];
|
|
1811
|
+
const trashed = snapshot ? snapshot.trashed : [];
|
|
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);
|
|
1819
|
+
const selectedRows = rows.filter((row) => selected.includes(row.sessionId));
|
|
1820
|
+
const groups = grouping ? groupRows(filtered, t) : [];
|
|
1821
|
+
const blockOf = (group) => {
|
|
1822
|
+
const key = collapseKey(tab, group.key);
|
|
1823
|
+
return { key, group, rows: isGroupCollapsed(key, collapsedOverrides, settings.groupDefault) ? [] : group.rows };
|
|
1824
|
+
};
|
|
1825
|
+
const blocks = grouping ? groups.map(blockOf) : [{ key: `${tab}:flat`, group: null, rows: filtered }];
|
|
1826
|
+
const tabs = [
|
|
1827
|
+
{ id: "archived", label: t(TAB_LABEL.archived), count: archived.length },
|
|
1828
|
+
{ id: "pinned", label: t(TAB_LABEL.pinned), count: pinned.length },
|
|
1829
|
+
{ id: "trash", label: t(TAB_LABEL.trash), count: trashed.length },
|
|
1830
|
+
{ id: "usage", label: t("titleUsage"), count: 0 },
|
|
1831
|
+
{ id: "config", label: t("titleConfig"), count: 0 }
|
|
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
|
+
};
|
|
1864
|
+
const rowMenu = (row) => {
|
|
1865
|
+
const running = row.active === true;
|
|
1866
|
+
const moveToTrash = {
|
|
1867
|
+
id: "trash",
|
|
1868
|
+
label: t("moveToTrash"),
|
|
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;
|
|
1887
|
+
};
|
|
1888
|
+
if (tab === "archived") {
|
|
1889
|
+
return {
|
|
1890
|
+
items: [
|
|
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
|
|
1895
|
+
],
|
|
1896
|
+
run: (id) => {
|
|
1897
|
+
setMenuFor(null);
|
|
1898
|
+
if (handleLocate(id)) return;
|
|
1899
|
+
if (id === "trash") requestTrash([row.sessionId], row.title);
|
|
1900
|
+
else void run(id, [row.sessionId]);
|
|
1901
|
+
}
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
if (tab === "pinned") {
|
|
1905
|
+
return {
|
|
1906
|
+
items: [
|
|
1907
|
+
{ id: "unpin", label: t("unpin"), icon: React8.createElement(import_dsh_client_ui_primitives6.IconPinOutlineRegular, { size: 14 }) },
|
|
1908
|
+
moveToTrash,
|
|
1909
|
+
...locate
|
|
1910
|
+
],
|
|
1911
|
+
run: (id) => {
|
|
1912
|
+
setMenuFor(null);
|
|
1913
|
+
if (handleLocate(id)) return;
|
|
1914
|
+
if (id === "trash") requestTrash([row.sessionId], row.title);
|
|
1915
|
+
else void run(id, [row.sessionId]);
|
|
1916
|
+
}
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
const restorable = row.restorable !== false;
|
|
1920
|
+
return {
|
|
1921
|
+
items: [
|
|
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
|
|
1930
|
+
],
|
|
1931
|
+
run: (id) => {
|
|
1932
|
+
setMenuFor(null);
|
|
1933
|
+
if (handleLocate(id)) return;
|
|
1934
|
+
if (id === "restore" && !restorable) return;
|
|
1935
|
+
if (id === "purge") requestPurge([row.sessionId], row.title);
|
|
1936
|
+
else void run(id, [row.sessionId]);
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
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
|
+
};
|
|
1947
|
+
const batch = [];
|
|
1948
|
+
if (tab === "archived" && selected.length > 0) {
|
|
1949
|
+
batch.push({ label: t("unarchive"), run: () => void run("unarchive", selected) });
|
|
1950
|
+
batch.push({
|
|
1951
|
+
label: t("moveToTrash"),
|
|
1952
|
+
variant: "outline",
|
|
1953
|
+
run: () => requestTrash(selected, selectedRows[0] ? selectedRows[0].title : "")
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
if (tab === "pinned" && selected.length > 0) {
|
|
1957
|
+
batch.push({ label: t("unpin"), run: () => void run("unpin", selected) });
|
|
1958
|
+
batch.push({
|
|
1959
|
+
label: t("moveToTrash"),
|
|
1960
|
+
variant: "outline",
|
|
1961
|
+
run: () => requestTrash(selected, selectedRows[0] ? selectedRows[0].title : "")
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
if (tab === "trash" && selected.length > 0) {
|
|
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
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
const totals = snapshot ? snapshot.totals : null;
|
|
1974
|
+
const totalBytes = tab === "archived" ? totals ? totals.archivedBytes : 0 : tab === "trash" ? totals ? totals.trashBytes : 0 : 0;
|
|
1975
|
+
const nodes = [];
|
|
1976
|
+
let first = true;
|
|
1977
|
+
for (const block of blocks) {
|
|
1978
|
+
if (block.group !== null) {
|
|
1979
|
+
const group = block.group;
|
|
1980
|
+
const key = block.key;
|
|
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
|
+
);
|
|
2003
|
+
first = false;
|
|
2004
|
+
}
|
|
2005
|
+
for (const row of block.rows) {
|
|
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
|
+
);
|
|
2024
|
+
first = false;
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
const isConfig = tab === "config";
|
|
2028
|
+
const isUsage = tab === "usage";
|
|
2029
|
+
const isDataTab = !isConfig && !isUsage;
|
|
2030
|
+
const configRow = (key, label, hint, control, firstRow) => React8.createElement(
|
|
2031
|
+
"div",
|
|
2032
|
+
{ key, style: firstRow ? { ...S.configRow, borderTop: "none" } : S.configRow },
|
|
2033
|
+
React8.createElement(
|
|
2034
|
+
"div",
|
|
2035
|
+
{ style: S.configMain },
|
|
2036
|
+
React8.createElement("span", { style: S.configLabel }, label),
|
|
2037
|
+
hint === null ? null : React8.createElement("span", { style: S.configHint }, hint)
|
|
2038
|
+
),
|
|
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
|
+
})
|
|
2095
|
+
);
|
|
2096
|
+
const version = snapshot !== null && typeof snapshot.version === "string" ? snapshot.version : update.value !== null && typeof update.value.current === "string" ? update.value.current : null;
|
|
2097
|
+
const outdated = update.state === "done" && update.value !== null && update.value.outdated === true;
|
|
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(
|
|
2104
|
+
"div",
|
|
2105
|
+
{ style: S.column },
|
|
2106
|
+
React8.createElement(
|
|
2107
|
+
"div",
|
|
2108
|
+
{ style: S.card },
|
|
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
|
+
),
|
|
2167
|
+
// 官方 store 只在创建时读一次 localStorage,所以改完必须刷新页面。
|
|
2168
|
+
sidebarArchived === sidebarAtMount ? null : React8.createElement(
|
|
2169
|
+
"div",
|
|
2170
|
+
{ key: "config:sidebar-refresh", style: S.configFoot },
|
|
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
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
},
|
|
2184
|
+
t("refreshPage")
|
|
2185
|
+
)
|
|
2186
|
+
),
|
|
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(
|
|
2256
|
+
"div",
|
|
2257
|
+
{ key: "config:reset", style: { ...S.configFoot, justifyContent: "flex-end" } },
|
|
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
|
+
)
|
|
2270
|
+
)
|
|
2271
|
+
),
|
|
2272
|
+
React8.createElement(
|
|
2273
|
+
"div",
|
|
2274
|
+
{ style: S.card },
|
|
2275
|
+
React8.createElement(
|
|
2276
|
+
"div",
|
|
2277
|
+
{ style: { ...S.configRow, borderTop: "none" } },
|
|
2278
|
+
React8.createElement(
|
|
2279
|
+
"div",
|
|
2280
|
+
{ style: S.configMain },
|
|
2281
|
+
React8.createElement("span", { style: S.configLabel }, t("aboutTitle")),
|
|
2282
|
+
React8.createElement("span", { style: S.configHint }, "dsh-session-curator")
|
|
2283
|
+
),
|
|
2284
|
+
React8.createElement(
|
|
2285
|
+
"div",
|
|
2286
|
+
{ style: S.configControl },
|
|
2287
|
+
// 版本号:浅色小字。
|
|
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
|
+
)
|
|
2299
|
+
)
|
|
2300
|
+
),
|
|
2301
|
+
updateLine === null ? null : React8.createElement("div", { key: "config:update", style: S.configFoot }, updateLine),
|
|
2302
|
+
outdated && update.value !== null ? React8.createElement(
|
|
2303
|
+
"div",
|
|
2304
|
+
{ key: "config:upgrade", style: S.updateBox },
|
|
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(
|
|
2311
|
+
"span",
|
|
2312
|
+
{ style: S.updateSteps },
|
|
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"))
|
|
2317
|
+
),
|
|
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
|
+
)
|
|
2328
|
+
) : null
|
|
2329
|
+
)
|
|
2330
|
+
);
|
|
2331
|
+
const usagePage = React8.createElement(
|
|
2332
|
+
"div",
|
|
2333
|
+
{ style: S.column },
|
|
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(
|
|
2366
|
+
"div",
|
|
2367
|
+
{ style: S.header },
|
|
2368
|
+
React8.createElement(
|
|
2369
|
+
"div",
|
|
2370
|
+
{ style: S.tabBar },
|
|
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
|
+
)
|
|
2383
|
+
),
|
|
2384
|
+
// 分组与刷新推到右边;只有三个**数据**页签上才有它们(配置页与磁盘占用页都没有
|
|
2385
|
+
// 可筛可选的列表,磁盘占用页自带一个只重拉 usage 的刷新按钮)。
|
|
2386
|
+
isDataTab ? React8.createElement(
|
|
2387
|
+
"div",
|
|
2388
|
+
{ style: S.headerActions },
|
|
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
|
|
2409
|
+
),
|
|
2410
|
+
error ? React8.createElement("div", { style: S.error }, error) : null,
|
|
2411
|
+
// 过滤工具条:只有数据页签上有(配置页没有列表可筛,磁盘占用页也不是列表)。
|
|
2412
|
+
isDataTab ? filterBar : null,
|
|
2413
|
+
isDataTab ? React8.createElement(
|
|
2414
|
+
"div",
|
|
2415
|
+
{ style: S.card },
|
|
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(
|
|
2423
|
+
"div",
|
|
2424
|
+
{ style: S.rowLine },
|
|
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,
|
|
2464
|
+
isConfig ? configPage : null,
|
|
2465
|
+
isDataTab ? React8.createElement(
|
|
2466
|
+
"div",
|
|
2467
|
+
{ style: S.footer },
|
|
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}`
|
|
2519
|
+
);
|
|
2520
|
+
}
|
|
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
|
+
|
|
2549
|
+
// src/client/index.ts
|
|
2550
|
+
var MENU_ID = "session-curator.delete";
|
|
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";
|
|
2555
|
+
var SECTION_ID = "session-curator";
|
|
2556
|
+
var NAV_SECTION_ID = "archived-sessions";
|
|
2557
|
+
function sectionIdFor(ctx) {
|
|
2558
|
+
try {
|
|
2559
|
+
const taken = ctx.slots.entries("settings.section").some((entry) => entry?.options?.id === NAV_SECTION_ID);
|
|
2560
|
+
return taken ? SECTION_ID : NAV_SECTION_ID;
|
|
2561
|
+
} catch {
|
|
2562
|
+
return SECTION_ID;
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
var inject = ["slots", "locale"];
|
|
2566
|
+
function apply(ctx) {
|
|
2567
|
+
setContext(ctx);
|
|
2568
|
+
ctx.effect(() => ctx.locale.register(NS, dictionaries), "dsh-session-curator: dictionaries");
|
|
2569
|
+
const t = ctx.locale.bind(NS);
|
|
2570
|
+
const contribute = (slot, options, component, label) => {
|
|
2571
|
+
try {
|
|
2572
|
+
ctx.slots.inject(slot, () => ctx.slots.register(options, component));
|
|
2573
|
+
} catch (error) {
|
|
2574
|
+
reportError(error);
|
|
2575
|
+
if (ctx.logger) ctx.logger.warn(`[session-curator] slot ${slot} 注册失败(${label})`);
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
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
|
+
);
|
|
2661
|
+
}
|
|
2662
|
+
})(module, module.exports, require);
|
|
2663
|
+
return module.exports;
|
|
1429
2664
|
}
|
|
1430
2665
|
});
|