@duke-dsh-plugins/dsh-archive-manager 1.3.5 → 1.4.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 +19 -1
- package/client.js +928 -530
- package/index.js +276 -276
- package/package.json +4 -3
- package/typert.host.js +278 -224
package/client.js
CHANGED
|
@@ -1,530 +1,928 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-archive-manager — Client half (web bundle).
|
|
3
|
-
*
|
|
4
|
-
* Rendered by the DSH web shell via `window.__ModuleLoader__.load`.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
.am-
|
|
57
|
-
|
|
58
|
-
/*
|
|
59
|
-
.am-
|
|
60
|
-
|
|
61
|
-
/*
|
|
62
|
-
.am-
|
|
63
|
-
.am-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
.am-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
.am-
|
|
70
|
-
.am-
|
|
71
|
-
.am-
|
|
72
|
-
.am-
|
|
73
|
-
|
|
74
|
-
/*
|
|
75
|
-
.am-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
.am-
|
|
79
|
-
.am-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
.am-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
.am-
|
|
86
|
-
.am-
|
|
87
|
-
.am-
|
|
88
|
-
.am-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
.am-
|
|
94
|
-
.am-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
.am-
|
|
98
|
-
.am-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
.am-
|
|
103
|
-
.am-
|
|
104
|
-
.am-
|
|
105
|
-
.am-
|
|
106
|
-
.am-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
.am-
|
|
110
|
-
.am-
|
|
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
|
-
const
|
|
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
|
-
React.createElement("
|
|
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
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* dsh-archive-manager — Client half (web bundle).
|
|
3
|
+
*
|
|
4
|
+
* Rendered by the DSH web shell via `window.__ModuleLoader__.load`. Two
|
|
5
|
+
* operating modes, chosen by feature detection (never by version string):
|
|
6
|
+
*
|
|
7
|
+
* - DSH < 0.1.7 (legacy): registers a sidebar footer action button
|
|
8
|
+
* (archived-session entry) plus a frame-wide archive manager panel
|
|
9
|
+
* (search / group / restore / delete), rendered together from the
|
|
10
|
+
* sidebar.footer.action slot — mirroring how ui-settings renders its modal
|
|
11
|
+
* from sidebar.settings (see the registration comment for the
|
|
12
|
+
* stacking-context rationale).
|
|
13
|
+
* - DSH >= 0.1.7 (modern): the shell ships its own archive management (the
|
|
14
|
+
* sidebar "仅显示已归档" filter with per-row archive actions), so this
|
|
15
|
+
* plugin adds ONLY the missing delete affordance: a danger row in each
|
|
16
|
+
* archived session's "..." menu (`sidebar.workspaces.session.menu.item`)
|
|
17
|
+
* and a hover trash button at the row end
|
|
18
|
+
* (`sidebar.workspaces.session.row.action`), both opening a confirm dialog
|
|
19
|
+
* rendered from the sidebar foot slot. Detection = whether those slots
|
|
20
|
+
* ever get declared: `ctx.slots.inject()` on an undeclared key never
|
|
21
|
+
* fires, so pre-0.1.7 builds stay on the legacy path automatically, and
|
|
22
|
+
* the footer button hides itself the moment the modern slots appear.
|
|
23
|
+
*
|
|
24
|
+
* The panel/dialog mirrors the DSH 工作区 (workspace) browsing region: a
|
|
25
|
+
* dialog surface using the same design tokens as the workspace browser —
|
|
26
|
+
* folder group headers, compact 32px session rows with a leading slot icon
|
|
27
|
+
* and relative time, and hover-revealed actions (workspace rowActions
|
|
28
|
+
* pattern).
|
|
29
|
+
*
|
|
30
|
+
* Host communication goes through the `archiveManager` Remote namespace
|
|
31
|
+
* (`ctx.remote.archiveManager.restore/delete/state`), published by the Host
|
|
32
|
+
* half in `index.js`.
|
|
33
|
+
*/
|
|
34
|
+
window.__ModuleLoader__.load({
|
|
35
|
+
id: "@duke-dsh-plugins/dsh-archive-manager",
|
|
36
|
+
factory: (require) => {
|
|
37
|
+
var module = { exports: {} };
|
|
38
|
+
var exports = module.exports;
|
|
39
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
40
|
+
const React = require("react");
|
|
41
|
+
|
|
42
|
+
// ---- CSS (package-owned, mirrors the DSH workspace browser + Modal) ----
|
|
43
|
+
// CSS_COMMON renders in both modes; CSS_LEGACY is the footer-button
|
|
44
|
+
// dressing, injected only while the legacy panel exists (cleared the
|
|
45
|
+
// moment the 0.1.7+ slots declare, so the modern shell keeps its own
|
|
46
|
+
// footer layout untouched).
|
|
47
|
+
const CSS_COMMON = `
|
|
48
|
+
/* Overlay — same mask surface as DSH Modal, same z-index tier as the DSH
|
|
49
|
+
settings overlay (1000). This layer MUST be rendered from a slot that sits
|
|
50
|
+
outside any stacking context (the sidebar foot, like Settings does), never
|
|
51
|
+
from the shell.overlay slot: AppFrame renders that slot inside its
|
|
52
|
+
.overlayLayer (position:absolute;z-index:20, a stacking context), which
|
|
53
|
+
traps any child z-index below it — dsh-better-sidebar appends a
|
|
54
|
+
position:fixed;z-index:25 layer to document.body and would cover the
|
|
55
|
+
modal. */
|
|
56
|
+
.am-overlay{position:fixed;inset:0;z-index:1000;background:var(--dsw-alias-bg-mask-1,rgba(0,0,0,.4));backdrop-filter:var(--dsw-mask-blur,blur(2px));display:flex;align-items:center;justify-content:center;padding:24px;}
|
|
57
|
+
|
|
58
|
+
/* Panel — same surface & size as the DSH Settings dialog (width 800px) */
|
|
59
|
+
.am-panel{position:relative;display:flex;flex-direction:column;width:800px;max-width:calc(100vw - 48px);height:min(800px,100vh - 48px);overflow:hidden;border:1px solid var(--dsw-alias-border-inverted,rgba(128,128,128,.3));border-radius:24px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,inherit);box-shadow:var(--dsw-shadow-lv3,0 12px 40px rgba(0,0,0,.25));}
|
|
60
|
+
|
|
61
|
+
/* Header — DSH Modal header */
|
|
62
|
+
.am-header{display:flex;align-items:center;gap:10px;padding:18px 14px 10px 24px;}
|
|
63
|
+
.am-title{font-weight:500;font-size:16px;line-height:24px;flex:1;color:var(--dsw-alias-label-primary,inherit);}
|
|
64
|
+
.am-count{font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary,inherit);flex:none;}
|
|
65
|
+
.am-close{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:8px;background:transparent;cursor:pointer;color:var(--dsw-alias-label-secondary,inherit);padding:0;}
|
|
66
|
+
.am-close:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.12));color:var(--dsw-alias-label-primary,inherit);}
|
|
67
|
+
|
|
68
|
+
/* Search — workspace expanded search input */
|
|
69
|
+
.am-search{padding:0 24px 12px;}
|
|
70
|
+
.am-search input{box-sizing:border-box;width:100%;height:30px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.2));border-radius:10px;background:transparent;color:var(--dsw-alias-label-primary,inherit);font-size:13px;line-height:18px;outline:none;}
|
|
71
|
+
.am-search input::placeholder{color:var(--dsw-alias-label-tertiary,inherit);}
|
|
72
|
+
.am-search input:focus{border-color:var(--dsw-alias-brand-primary,#4a7dff);}
|
|
73
|
+
|
|
74
|
+
/* Body — workspace list area */
|
|
75
|
+
.am-body{flex:1;overflow-y:auto;padding:4px 8px 16px;overscroll-behavior:contain;}
|
|
76
|
+
|
|
77
|
+
/* Group sections, spaced like workspace group sections */
|
|
78
|
+
.am-group{margin-top:2px;}
|
|
79
|
+
.am-group+.am-group{margin-top:4px;}
|
|
80
|
+
|
|
81
|
+
/* Group head — workspace project row (folder + label + count), clickable + collapsible */
|
|
82
|
+
.am-group-head{box-sizing:border-box;display:flex;align-items:center;gap:6px;height:34px;width:100%;padding:0 8px;border-radius:8px;border:none;background:transparent;color:inherit;font-family:inherit;font-size:inherit;text-align:left;cursor:pointer;}
|
|
83
|
+
.am-group-head:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.08));}
|
|
84
|
+
.am-chevron{flex:none;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary,inherit);transition:transform .15s var(--ds-ease-in-out);}
|
|
85
|
+
.am-chevron.am-open{transform:rotate(90deg);}
|
|
86
|
+
.am-group-icon{flex:none;width:16px;height:20px;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary,inherit);}
|
|
87
|
+
.am-group-label{flex:1;min-width:0;font-size:14px;line-height:20px;color:var(--dsw-alias-label-primary,inherit);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
|
88
|
+
.am-group-count{flex:none;font-size:12px;line-height:20px;color:var(--dsw-alias-label-tertiary,inherit);}
|
|
89
|
+
|
|
90
|
+
/* Session row — workspace session row (32px, radius 8, hover bg), indented under its group */
|
|
91
|
+
.am-row{display:flex;align-items:center;gap:6px;height:32px;padding:0 8px 0 32px;border-radius:8px;color:var(--dsw-alias-label-primary,inherit);}
|
|
92
|
+
.am-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.08));}
|
|
93
|
+
.am-slot{flex:none;width:16px;height:20px;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary,inherit);}
|
|
94
|
+
.am-title{flex:1;min-width:0;font-size:14px;line-height:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
|
95
|
+
.am-time{flex:none;font-size:12px;line-height:20px;color:var(--dsw-alias-label-tertiary,inherit);}
|
|
96
|
+
.am-row-actions{flex:none;display:none;align-items:center;gap:2px;}
|
|
97
|
+
.am-row:hover .am-time{display:none;}
|
|
98
|
+
.am-row:hover .am-row-actions{display:inline-flex;}
|
|
99
|
+
|
|
100
|
+
/* Action buttons — subtle text buttons, hover-revealed like workspace rowActions */
|
|
101
|
+
.am-btn{border:none;background:transparent;font-size:12px;line-height:18px;padding:4px 8px;border-radius:8px;cursor:pointer;white-space:nowrap;}
|
|
102
|
+
.am-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.12));}
|
|
103
|
+
.am-btn-restore{color:var(--dsw-alias-brand-primary,#4a7dff);}
|
|
104
|
+
.am-btn-danger{color:var(--dsw-alias-state-error-primary,#e5484d);}
|
|
105
|
+
.am-btn-danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger,rgba(229,72,77,.1));}
|
|
106
|
+
.am-btn[disabled]{opacity:.5;cursor:default;}
|
|
107
|
+
|
|
108
|
+
/* Orphan rows (log gone) */
|
|
109
|
+
.am-orphan .am-slot{color:var(--dsw-alias-state-warn-primary,#d99a06);}
|
|
110
|
+
.am-orphan .am-title{color:var(--dsw-alias-label-secondary,inherit);}
|
|
111
|
+
|
|
112
|
+
/* Empty / error / ghost — workspace empty style */
|
|
113
|
+
.am-empty{color:var(--dsw-alias-label-tertiary,inherit);padding:16px 12px;font-size:13px;line-height:20px;text-align:center;}
|
|
114
|
+
.am-error{color:var(--dsw-alias-state-error-primary,#e5484d);padding:8px 24px;font-size:12px;line-height:18px;}
|
|
115
|
+
.am-ghost{color:var(--dsw-alias-label-secondary,inherit);padding:8px 24px;font-size:12px;line-height:18px;}
|
|
116
|
+
|
|
117
|
+
/* ---- 0.1.7+ modern surfaces (delete affordances on the built-in archive) ---- */
|
|
118
|
+
|
|
119
|
+
/* Danger row in an archived session's "..." menu. Rendered through
|
|
120
|
+
ui-primitives' MenuItemButton when the module table provides it (native
|
|
121
|
+
look); this class styles the defensive plain-button fallback. */
|
|
122
|
+
.am-menu-del{display:flex;align-items:center;gap:8px;width:100%;padding:5px 12px;border:none;background:transparent;color:var(--dsw-alias-state-error-primary,#e5484d);font-family:inherit;font-size:13px;line-height:18px;text-align:left;cursor:pointer;border-radius:var(--dsw-radius-sm,6px);}
|
|
123
|
+
.am-menu-del:hover{background:var(--dsw-alias-interactive-bg-hover-danger,rgba(229,72,77,.12));}
|
|
124
|
+
.am-menu-del[disabled]{opacity:.5;cursor:default;}
|
|
125
|
+
|
|
126
|
+
/* Hover button at the row end — same 28px metrics as ui-workspace's own
|
|
127
|
+
row-action iconButton so it sits in the strip without reflow. */
|
|
128
|
+
.am-row-del{border-radius:var(--dsw-radius-sm,6px);cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-secondary,inherit);background:0 0;border:none;flex:none;display:inline-flex;justify-content:center;align-items:center;padding:0;}
|
|
129
|
+
.am-row-del:hover{color:var(--dsw-alias-state-error-primary,#e5484d);}
|
|
130
|
+
.am-row-del[disabled]{opacity:.5;cursor:default;}
|
|
131
|
+
|
|
132
|
+
/* Confirm dialog — same surface family as the legacy panel, compact width.
|
|
133
|
+
Rendered from the sidebar foot slot (fixed, root stacking context) so
|
|
134
|
+
dsh-better-sidebar's z-index:25 body layer cannot cover it. */
|
|
135
|
+
.am-confirm{position:relative;display:flex;flex-direction:column;width:440px;max-width:calc(100vw - 48px);overflow:hidden;border:1px solid var(--dsw-alias-border-inverted,rgba(128,128,128,.3));border-radius:24px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,inherit);box-shadow:var(--dsw-shadow-lv3,0 12px 40px rgba(0,0,0,.25));}
|
|
136
|
+
.am-confirm .am-confirm-body{padding:4px 24px 16px;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary,inherit);}
|
|
137
|
+
.am-confirm .am-confirm-title{font-weight:500;font-size:16px;line-height:24px;color:var(--dsw-alias-label-primary,inherit);}
|
|
138
|
+
.am-confirm .am-confirm-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:0 24px 20px;}
|
|
139
|
+
.am-confirm .am-confirm-error{color:var(--dsw-alias-state-error-primary,#e5484d);font-size:12px;line-height:18px;padding:0 24px 12px;}
|
|
140
|
+
.am-confirm .am-confirm-note{color:var(--dsw-alias-label-tertiary,inherit);font-size:12px;line-height:18px;}
|
|
141
|
+
.am-confirm .am-confirm-btn{border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.2));background:transparent;color:var(--dsw-alias-label-primary,inherit);font-family:inherit;font-size:13px;line-height:18px;padding:6px 14px;border-radius:10px;cursor:pointer;}
|
|
142
|
+
.am-confirm .am-confirm-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.12));}
|
|
143
|
+
.am-confirm .am-confirm-btn.am-confirm-danger{border-color:transparent;background:var(--dsw-alias-state-error-primary,#e5484d);color:var(--dsw-alias-bg-base,#fff);}
|
|
144
|
+
.am-confirm .am-confirm-btn.am-confirm-danger:hover{background:var(--dsw-alias-state-error-primary,#e5484d);filter:brightness(1.06);}
|
|
145
|
+
.am-confirm .am-confirm-btn[disabled]{opacity:.5;cursor:default;}
|
|
146
|
+
|
|
147
|
+
/* Sidebar footer entry (legacy mode only) */
|
|
148
|
+
.am-foot-layer{flex:none;width:100%;margin:8px 0 0;display:flex;align-items:center;}
|
|
149
|
+
.am-foot-layer .am-foot{width:100%;height:49px;border-radius:12px;padding:0 8px 0 6px;display:inline-flex;align-items:center;gap:8px;background:transparent;border:none;color:var(--dsw-alias-label-primary,inherit);font-family:inherit;font-size:14px;cursor:pointer;overflow:hidden;}
|
|
150
|
+
.am-foot-layer .am-foot:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.1));}
|
|
151
|
+
.am-foot-layer .am-foot.am-active{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.14));}
|
|
152
|
+
.am-foot-layer .am-foot .am-foot-icon{flex:none;display:inline-flex;align-items:center;justify-content:center;}
|
|
153
|
+
.am-foot-layer .am-foot .am-foot-label{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden;}
|
|
154
|
+
.am-foot-layer .am-foot .am-badge{color:var(--dsw-alias-label-tertiary,inherit);font-variant-numeric:tabular-nums;flex:none;margin-left:auto;font-size:12px;line-height:16px;}
|
|
155
|
+
.am-foot-layer.am-foot-rail{width:36px;height:36px;margin:0;}
|
|
156
|
+
.am-foot-layer.am-foot-rail .am-foot{width:36px;height:36px;padding:0;justify-content:center;border-radius:10px;}
|
|
157
|
+
`;
|
|
158
|
+
|
|
159
|
+
// Footer-column layout: only meaningful while OUR footer button exists.
|
|
160
|
+
// Injected at boot, cleared permanently once the 0.1.7+ slots declare
|
|
161
|
+
// (markModern), so a modern DSH footer keeps its own layout rules.
|
|
162
|
+
const CSS_LEGACY = `
|
|
163
|
+
[class*="footerActions"]{flex-direction:column;align-items:center;}
|
|
164
|
+
`;
|
|
165
|
+
|
|
166
|
+
// ---- Client Remote contribution ----------------------------------------
|
|
167
|
+
// The browser-side `remote.archiveManager` service only exists after this
|
|
168
|
+
// module mounts its namespace via ctx.remote.$mount(): dsh-api-remotes'
|
|
169
|
+
// client assembly mounts only the five official namespaces, so a plugin
|
|
170
|
+
// must mount its own. Mirrors the invocations in typert.host.js (ids,
|
|
171
|
+
// service/namespace/method, wire fields). zod is not requirable in the
|
|
172
|
+
// browser module loader, so codecs use passthrough schemas — the runtime
|
|
173
|
+
// contract only needs typeSymbol plus a parse (DSH < 0.1.7 reads
|
|
174
|
+
// `codec.schema.parse`) or a create() factory (the typert registry on
|
|
175
|
+
// DSH >= 0.1.7 rejects a strict codec with "has no create() factory").
|
|
176
|
+
// Every codec therefore carries BOTH fields, like typert.host.js.
|
|
177
|
+
let passthroughValue;
|
|
178
|
+
const passthroughSchema = () => (passthroughValue ??= { parse: (v) => v });
|
|
179
|
+
const strictClientCodec = (typeSymbol) => ({
|
|
180
|
+
mode: "strict",
|
|
181
|
+
typeSymbol,
|
|
182
|
+
schema: passthroughSchema(),
|
|
183
|
+
create: passthroughSchema,
|
|
184
|
+
});
|
|
185
|
+
const CLIENT_REMOTE = {
|
|
186
|
+
package: "dsh-archive-manager",
|
|
187
|
+
descriptors: [
|
|
188
|
+
{
|
|
189
|
+
id: "dsh-archive-manager#archiveManager/restore",
|
|
190
|
+
service: "archiveManager",
|
|
191
|
+
namespace: "archiveManager",
|
|
192
|
+
method: "restore",
|
|
193
|
+
invocation: { kind: "direct" },
|
|
194
|
+
parameters: [
|
|
195
|
+
{
|
|
196
|
+
name: "request",
|
|
197
|
+
wire: "request",
|
|
198
|
+
source: "json",
|
|
199
|
+
codec: strictClientCodec("dsh-archive-manager#ArchiveManagerRestoreRequest"),
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
result: strictClientCodec("dsh-archive-manager#ArchiveManagerRestoreResult"),
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: "dsh-archive-manager#archiveManager/delete",
|
|
206
|
+
service: "archiveManager",
|
|
207
|
+
namespace: "archiveManager",
|
|
208
|
+
method: "delete",
|
|
209
|
+
invocation: { kind: "direct" },
|
|
210
|
+
parameters: [
|
|
211
|
+
{
|
|
212
|
+
name: "request",
|
|
213
|
+
wire: "request",
|
|
214
|
+
source: "json",
|
|
215
|
+
codec: strictClientCodec("dsh-archive-manager#ArchiveManagerDeleteRequest"),
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
result: strictClientCodec("dsh-archive-manager#ArchiveManagerDeleteResult"),
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: "dsh-archive-manager#archiveManager/state",
|
|
222
|
+
service: "archiveManager",
|
|
223
|
+
namespace: "archiveManager",
|
|
224
|
+
method: "state",
|
|
225
|
+
invocation: { kind: "direct" },
|
|
226
|
+
parameters: [],
|
|
227
|
+
result: strictClientCodec("dsh-archive-manager#ArchiveManagerStateResult"),
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
async function apply(ctx) {
|
|
233
|
+
// Mount the archiveManager namespace before anything touches it; the
|
|
234
|
+
// mount's lifetime is bound to this plugin's context by $mount itself.
|
|
235
|
+
await ctx.remote.$mount(CLIENT_REMOTE);
|
|
236
|
+
|
|
237
|
+
const styleTag = document.createElement("style");
|
|
238
|
+
styleTag.textContent = CSS_COMMON;
|
|
239
|
+
document.head.appendChild(styleTag);
|
|
240
|
+
ctx.effect(() => () => styleTag.remove());
|
|
241
|
+
|
|
242
|
+
// Legacy footer-column override; cleared for good by markModern() once
|
|
243
|
+
// the 0.1.7+ built-in archive slots declare.
|
|
244
|
+
const legacyStyleTag = document.createElement("style");
|
|
245
|
+
legacyStyleTag.textContent = CSS_LEGACY;
|
|
246
|
+
document.head.appendChild(legacyStyleTag);
|
|
247
|
+
ctx.effect(() => () => legacyStyleTag.remove());
|
|
248
|
+
|
|
249
|
+
// ctx.get() reads the service without the property-accessor inject
|
|
250
|
+
// guard; it exists because the $mount above just created it.
|
|
251
|
+
const remote = ctx.get("remote.archiveManager");
|
|
252
|
+
|
|
253
|
+
// ui-primitives (MenuItemButton / Tooltip / IconTrashOutlineRegular) is
|
|
254
|
+
// part of the 0.1.7+ module table — ui-workspace requires it from the
|
|
255
|
+
// same table. Loaded defensively: pre-0.1.7 builds may not ship it, and
|
|
256
|
+
// the modern components (its only consumers) render only when the 0.1.7
|
|
257
|
+
// slots exist. Fallbacks below keep every surface functional without it.
|
|
258
|
+
let ui = null;
|
|
259
|
+
try {
|
|
260
|
+
ui = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
261
|
+
} catch {
|
|
262
|
+
ui = null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Shared open state between the footer button and the overlay.
|
|
266
|
+
let open = false;
|
|
267
|
+
const listeners = new Set();
|
|
268
|
+
function getOpen() { return open; }
|
|
269
|
+
function setOpen(v) { if (open !== v) { open = v; for (const fn of listeners) fn(); } }
|
|
270
|
+
function subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); }
|
|
271
|
+
function useOpen() { return React.useSyncExternalStore(subscribe, getOpen); }
|
|
272
|
+
|
|
273
|
+
// Ghost state (deleted-but-resident archived sessions), shared by both components.
|
|
274
|
+
const ghostStore = (() => {
|
|
275
|
+
let ids = [];
|
|
276
|
+
const listeners = new Set();
|
|
277
|
+
return {
|
|
278
|
+
getSnapshot: () => ids,
|
|
279
|
+
subscribe: (fn) => { listeners.add(fn); return () => listeners.delete(fn); },
|
|
280
|
+
set: (next) => { ids = next; for (const fn of listeners) fn(); },
|
|
281
|
+
};
|
|
282
|
+
})();
|
|
283
|
+
function useGhostIds() { return React.useSyncExternalStore(ghostStore.subscribe, ghostStore.getSnapshot); }
|
|
284
|
+
async function refreshState() {
|
|
285
|
+
try {
|
|
286
|
+
const res = await remote.state();
|
|
287
|
+
ghostStore.set(res && res.ok && Array.isArray(res.value.ghostIds) ? res.value.ghostIds : []);
|
|
288
|
+
} catch {
|
|
289
|
+
ghostStore.set([]);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ---- Modern (DSH >= 0.1.7) mode state --------------------------------
|
|
294
|
+
// modernStore flips on exactly when the built-in archive's row slots
|
|
295
|
+
// declare (see the inject registrations at the bottom of apply). Until
|
|
296
|
+
// then the legacy footer button renders; afterwards it hides itself and
|
|
297
|
+
// the footer slot only carries the delete confirm dialog.
|
|
298
|
+
const modernStore = (() => {
|
|
299
|
+
let value = false;
|
|
300
|
+
const listeners = new Set();
|
|
301
|
+
return {
|
|
302
|
+
getSnapshot: () => value,
|
|
303
|
+
subscribe: (fn) => { listeners.add(fn); return () => listeners.delete(fn); },
|
|
304
|
+
set: (next) => { if (value !== next) { value = next; for (const fn of listeners) fn(); } },
|
|
305
|
+
};
|
|
306
|
+
})();
|
|
307
|
+
function useModern() { return React.useSyncExternalStore(modernStore.subscribe, modernStore.getSnapshot); }
|
|
308
|
+
|
|
309
|
+
// Pending delete request for the modern confirm dialog ({sessionId,
|
|
310
|
+
// displayTitle} | null), set by the menu row / hover button.
|
|
311
|
+
const deleteStore = (() => {
|
|
312
|
+
let value = null;
|
|
313
|
+
const listeners = new Set();
|
|
314
|
+
return {
|
|
315
|
+
getSnapshot: () => value,
|
|
316
|
+
subscribe: (fn) => { listeners.add(fn); return () => listeners.delete(fn); },
|
|
317
|
+
set: (next) => { if (value !== next) { value = next; for (const fn of listeners) fn(); } },
|
|
318
|
+
};
|
|
319
|
+
})();
|
|
320
|
+
function useDeleteRequest() { return React.useSyncExternalStore(deleteStore.subscribe, deleteStore.getSnapshot); }
|
|
321
|
+
|
|
322
|
+
// Enter modern mode: hide the legacy button (and its footer-layout CSS)
|
|
323
|
+
// for good, then let the caller register its slot entries.
|
|
324
|
+
const markModern = () => {
|
|
325
|
+
modernStore.set(true);
|
|
326
|
+
legacyStyleTag.textContent = "";
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Whether one session is in the registry-global archive set, read from
|
|
331
|
+
* the framework's global `useWorkspaces` standard hook (delivered to
|
|
332
|
+
* every slot component on every supported DSH). Fails closed to false
|
|
333
|
+
* when the hook is unavailable.
|
|
334
|
+
*
|
|
335
|
+
* @param {Function} useWorkspaces - the global workspace snapshot hook.
|
|
336
|
+
* @param {string} sessionId - row session id.
|
|
337
|
+
* @returns {boolean} archived membership.
|
|
338
|
+
*/
|
|
339
|
+
function useArchivedMember(useWorkspaces, sessionId) {
|
|
340
|
+
try {
|
|
341
|
+
return useWorkspaces(
|
|
342
|
+
(s) => Array.isArray(s.archivedSessionIds) && s.archivedSessionIds.indexOf(sessionId) !== -1
|
|
343
|
+
) === true;
|
|
344
|
+
} catch {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function ArchiveIcon() {
|
|
350
|
+
return React.createElement(
|
|
351
|
+
"svg",
|
|
352
|
+
{ width: 16, height: 16, viewBox: "0 0 14 14", fill: "none", stroke: "currentColor", strokeWidth: 1.1, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
|
|
353
|
+
React.createElement("rect", { x: 1.5, y: 2, width: 11, height: 3.5, rx: 0.8 }),
|
|
354
|
+
React.createElement("path", { d: "M2.5 5.5v5a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1v-5" }),
|
|
355
|
+
React.createElement("path", { d: "M5.5 8h3" })
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function FolderIcon() {
|
|
360
|
+
return React.createElement(
|
|
361
|
+
"svg",
|
|
362
|
+
{ width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.3, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
|
|
363
|
+
React.createElement("path", { d: "M1.5 4.5a1 1 0 0 1 1-1h3l1.5 2h6.5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1v-7Z" })
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function WarningIcon() {
|
|
368
|
+
return React.createElement(
|
|
369
|
+
"svg",
|
|
370
|
+
{ width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.3, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
|
|
371
|
+
React.createElement("path", { d: "M8 2.5 14.5 13.5h-13L8 2.5Z" }),
|
|
372
|
+
React.createElement("path", { d: "M8 6.5v3" }),
|
|
373
|
+
React.createElement("circle", { cx: 8, cy: 11.5, r: 0.4, fill: "currentColor" })
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function ChevronIcon() {
|
|
378
|
+
return React.createElement(
|
|
379
|
+
"svg",
|
|
380
|
+
{ width: 14, height: 14, viewBox: "0 0 14 14", fill: "none", stroke: "currentColor", strokeWidth: 1.4, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
|
|
381
|
+
React.createElement("path", { d: "m5 3.5 3.5 3.5L5 10.5" })
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Trash glyph for the modern delete surfaces: ui-primitives' shipped
|
|
386
|
+
// icon when available (identical to the shell's own artwork), a plain
|
|
387
|
+
// outline SVG otherwise.
|
|
388
|
+
function TrashIcon({ size }) {
|
|
389
|
+
const px = size || 14;
|
|
390
|
+
if (ui && ui.IconTrashOutlineRegular) {
|
|
391
|
+
return React.createElement(ui.IconTrashOutlineRegular, { size: px });
|
|
392
|
+
}
|
|
393
|
+
return React.createElement(
|
|
394
|
+
"svg",
|
|
395
|
+
{ width: px, height: px, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.3, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
|
|
396
|
+
React.createElement("path", { d: "M2.5 4.5h11" }),
|
|
397
|
+
React.createElement("path", { d: "M5.5 4.5v-1a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v1" }),
|
|
398
|
+
React.createElement("path", { d: "M4 4.5l.6 8.1a1 1 0 0 0 1 .9h4.8a1 1 0 0 0 1-.9l.6-8.1" }),
|
|
399
|
+
React.createElement("path", { d: "M6.5 7v4M9.5 7v4" })
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function fmtTime(ts) {
|
|
404
|
+
if (typeof ts !== "number" || !isFinite(ts) || ts <= 0) return "";
|
|
405
|
+
const diff = Date.now() - ts;
|
|
406
|
+
const min = 60 * 1000, hour = 60 * min, day = 24 * hour, month = 30 * day;
|
|
407
|
+
if (diff < min) return "刚刚";
|
|
408
|
+
if (diff < hour) return Math.floor(diff / min) + " 分钟前";
|
|
409
|
+
if (diff < day) return Math.floor(diff / hour) + " 小时前";
|
|
410
|
+
if (diff < month) return Math.floor(diff / day) + " 天前";
|
|
411
|
+
const d = new Date(ts);
|
|
412
|
+
return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function deriveGroups(byId, ids, workspaces, archivedIds, query) {
|
|
416
|
+
const q = (query || "").trim().toLowerCase();
|
|
417
|
+
const archived = new Set(archivedIds);
|
|
418
|
+
const groups = [];
|
|
419
|
+
const accounted = new Set();
|
|
420
|
+
const resolved = new Set();
|
|
421
|
+
for (const ws of workspaces) {
|
|
422
|
+
const members = [];
|
|
423
|
+
for (const sid of ws.sessionIds) {
|
|
424
|
+
if (!archived.has(sid)) continue;
|
|
425
|
+
resolved.add(sid);
|
|
426
|
+
const s = byId[sid];
|
|
427
|
+
if (s === undefined) continue;
|
|
428
|
+
accounted.add(sid);
|
|
429
|
+
if (s.blank) continue;
|
|
430
|
+
const label = s.displayTitle || s.title || "未命名会话";
|
|
431
|
+
if (q && label.toLowerCase().indexOf(q) === -1 && ws.title.toLowerCase().indexOf(q) === -1 && String(s.cwd || "").toLowerCase().indexOf(q) === -1) continue;
|
|
432
|
+
members.push(s);
|
|
433
|
+
}
|
|
434
|
+
if (members.length === 0) continue;
|
|
435
|
+
members.sort((a, b) => b.updatedAt - a.updatedAt || (a.id < b.id ? -1 : 1));
|
|
436
|
+
groups.push({ key: ws.workspaceId, label: ws.title, sessions: members });
|
|
437
|
+
}
|
|
438
|
+
const stray = [];
|
|
439
|
+
for (const sid of ids) {
|
|
440
|
+
if (accounted.has(sid)) continue;
|
|
441
|
+
if (!archived.has(sid)) continue;
|
|
442
|
+
resolved.add(sid);
|
|
443
|
+
const s = byId[sid];
|
|
444
|
+
if (s === undefined) continue;
|
|
445
|
+
if (s.blank) continue;
|
|
446
|
+
const label = s.displayTitle || s.title || "未命名会话";
|
|
447
|
+
if (q && label.toLowerCase().indexOf(q) === -1 && String(s.cwd || "").toLowerCase().indexOf(q) === -1) continue;
|
|
448
|
+
stray.push(s);
|
|
449
|
+
}
|
|
450
|
+
if (stray.length > 0) {
|
|
451
|
+
stray.sort((a, b) => b.updatedAt - a.updatedAt || (a.id < b.id ? -1 : 1));
|
|
452
|
+
groups.push({ key: "__ungrouped__", label: "未分组", sessions: stray });
|
|
453
|
+
}
|
|
454
|
+
const orphans = [];
|
|
455
|
+
for (const sid of archivedIds) {
|
|
456
|
+
if (resolved.has(sid)) continue;
|
|
457
|
+
const s = byId[sid];
|
|
458
|
+
if (s !== undefined && !s.blank) continue;
|
|
459
|
+
if (q) {
|
|
460
|
+
const sidLower = String(sid).toLowerCase();
|
|
461
|
+
if (sidLower.indexOf(q) === -1) continue;
|
|
462
|
+
}
|
|
463
|
+
orphans.push({ id: sid, orphan: true, displayTitle: "未知会话(日志已不存在)", updatedAt: 0, cwd: undefined });
|
|
464
|
+
}
|
|
465
|
+
return { groups, orphans };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function ArchiveManager({ slotProps, onClose }) {
|
|
469
|
+
const [query, setQuery] = React.useState("");
|
|
470
|
+
const [busy, setBusy] = React.useState(null);
|
|
471
|
+
const [confirming, setConfirming] = React.useState(null);
|
|
472
|
+
const [error, setError] = React.useState(null);
|
|
473
|
+
// Groups are collapsed by default so a large archive stays manageable;
|
|
474
|
+
// a non-empty search query force-expands every group to show matches.
|
|
475
|
+
const [expanded, setExpanded] = React.useState(() => new Set());
|
|
476
|
+
const ghostIds = useGhostIds();
|
|
477
|
+
const rawArchivedIds = slotProps.useWorkspaces((s) => s.archivedSessionIds);
|
|
478
|
+
const byId = slotProps.useSessions((s) => s.byId);
|
|
479
|
+
const ids = slotProps.useSessions((s) => s.ids);
|
|
480
|
+
const workspaces = slotProps.useWorkspaces((s) => s.items);
|
|
481
|
+
React.useEffect(() => { refreshState(); }, []);
|
|
482
|
+
const archivedIds = React.useMemo(() => {
|
|
483
|
+
if (ghostIds.length === 0) return rawArchivedIds;
|
|
484
|
+
const ghost = new Set(ghostIds);
|
|
485
|
+
return rawArchivedIds.filter((id) => !ghost.has(id));
|
|
486
|
+
}, [rawArchivedIds, ghostIds]);
|
|
487
|
+
const derived = React.useMemo(() => {
|
|
488
|
+
try {
|
|
489
|
+
const d = deriveGroups(byId, ids, workspaces, archivedIds, query);
|
|
490
|
+
return { groups: d.groups, orphans: d.orphans, error: null };
|
|
491
|
+
} catch (e) {
|
|
492
|
+
return { groups: [], orphans: [], error: e instanceof Error ? e.message : String(e) };
|
|
493
|
+
}
|
|
494
|
+
}, [byId, ids, workspaces, archivedIds, query]);
|
|
495
|
+
const runAction = async (action, sessionId) => {
|
|
496
|
+
setBusy(action + ":" + sessionId);
|
|
497
|
+
setError(null);
|
|
498
|
+
try {
|
|
499
|
+
const res = await remote[action]({ sessionId });
|
|
500
|
+
if (res && res.ok) {
|
|
501
|
+
if (action === "delete") {
|
|
502
|
+
try {
|
|
503
|
+
const sessions = ctx.get("sessions");
|
|
504
|
+
if (sessions && typeof sessions.refresh === "function") sessions.refresh();
|
|
505
|
+
} catch { /* non-fatal */ }
|
|
506
|
+
refreshState();
|
|
507
|
+
}
|
|
508
|
+
} else {
|
|
509
|
+
const err = res && res.error;
|
|
510
|
+
setError((err && err.message) || (err && err.code) || "操作失败");
|
|
511
|
+
}
|
|
512
|
+
} catch (e) {
|
|
513
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
514
|
+
} finally {
|
|
515
|
+
setBusy(null);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
const total = archivedIds.length;
|
|
519
|
+
const groups = derived.groups.slice();
|
|
520
|
+
if (derived.orphans.length > 0) {
|
|
521
|
+
groups.push({ key: "__orphan__", label: "无效的归档记录", orphanGroup: true, sessions: derived.orphans });
|
|
522
|
+
}
|
|
523
|
+
const searching = query.trim() !== "";
|
|
524
|
+
const isExpanded = (key) => searching || expanded.has(key);
|
|
525
|
+
const toggleGroup = (key) => {
|
|
526
|
+
setExpanded((prev) => {
|
|
527
|
+
const next = new Set(prev);
|
|
528
|
+
if (next.has(key)) next.delete(key); else next.add(key);
|
|
529
|
+
return next;
|
|
530
|
+
});
|
|
531
|
+
};
|
|
532
|
+
const empty = total === 0 && ghostIds.length === 0;
|
|
533
|
+
const allGhost = total === 0 && ghostIds.length > 0;
|
|
534
|
+
const header = React.createElement(
|
|
535
|
+
"div", { className: "am-header" },
|
|
536
|
+
React.createElement("span", { className: "am-title" }, "归档管理"),
|
|
537
|
+
React.createElement("span", { className: "am-count" }, total + " 个归档"),
|
|
538
|
+
typeof onClose === "function"
|
|
539
|
+
? React.createElement("button", { className: "am-close", onClick: onClose, title: "关闭", "aria-label": "关闭" }, "✕")
|
|
540
|
+
: null
|
|
541
|
+
);
|
|
542
|
+
const search = React.createElement(
|
|
543
|
+
"div", { className: "am-search" },
|
|
544
|
+
React.createElement("input", {
|
|
545
|
+
type: "text",
|
|
546
|
+
placeholder: "搜索归档标题或项目…",
|
|
547
|
+
value: query,
|
|
548
|
+
onChange: (e) => { setQuery(e.target.value); setConfirming(null); },
|
|
549
|
+
})
|
|
550
|
+
);
|
|
551
|
+
const body = React.createElement(
|
|
552
|
+
"div", { className: "am-body" },
|
|
553
|
+
derived.error
|
|
554
|
+
? React.createElement("div", { className: "am-error" }, "渲染出错:" + derived.error)
|
|
555
|
+
: empty
|
|
556
|
+
? React.createElement("div", { className: "am-empty" }, "暂无归档会话")
|
|
557
|
+
: allGhost
|
|
558
|
+
? React.createElement("div", { className: "am-empty" }, "归档均已删除,重启 DSH 后自动清除")
|
|
559
|
+
: groups.map((g) =>
|
|
560
|
+
React.createElement(
|
|
561
|
+
"div", { className: "am-group", key: g.key },
|
|
562
|
+
React.createElement(
|
|
563
|
+
"button", {
|
|
564
|
+
type: "button",
|
|
565
|
+
className: "am-group-head",
|
|
566
|
+
onClick: () => toggleGroup(g.key),
|
|
567
|
+
"aria-expanded": isExpanded(g.key),
|
|
568
|
+
},
|
|
569
|
+
React.createElement(
|
|
570
|
+
"span", { className: "am-chevron" + (isExpanded(g.key) ? " am-open" : ""), "aria-hidden": true },
|
|
571
|
+
React.createElement(ChevronIcon)
|
|
572
|
+
),
|
|
573
|
+
React.createElement(
|
|
574
|
+
"span", { className: "am-group-icon", "aria-hidden": true },
|
|
575
|
+
g.orphanGroup ? React.createElement(WarningIcon) : React.createElement(FolderIcon)
|
|
576
|
+
),
|
|
577
|
+
React.createElement("span", { className: "am-group-label" }, g.label),
|
|
578
|
+
React.createElement("span", { className: "am-group-count" }, g.sessions.length + " 个")
|
|
579
|
+
),
|
|
580
|
+
isExpanded(g.key) ? g.sessions.map((s) => {
|
|
581
|
+
const restoring = busy === "restore:" + s.id;
|
|
582
|
+
const deleting = busy === "delete:" + s.id;
|
|
583
|
+
const isConfirm = confirming === s.id;
|
|
584
|
+
const isOrphan = s.orphan === true;
|
|
585
|
+
const timeLabel = isOrphan ? s.id : fmtTime(s.updatedAt);
|
|
586
|
+
const titleAttr = isOrphan ? s.id : (s.cwd || undefined);
|
|
587
|
+
const actions = [];
|
|
588
|
+
if (!isOrphan) {
|
|
589
|
+
actions.push(React.createElement("button", {
|
|
590
|
+
key: "restore",
|
|
591
|
+
type: "button",
|
|
592
|
+
className: "am-btn am-btn-restore",
|
|
593
|
+
disabled: !!busy,
|
|
594
|
+
onClick: () => runAction("restore", s.id),
|
|
595
|
+
}, restoring ? "…" : "还原"));
|
|
596
|
+
}
|
|
597
|
+
actions.push(React.createElement("button", {
|
|
598
|
+
key: "delete",
|
|
599
|
+
type: "button",
|
|
600
|
+
className: "am-btn am-btn-danger",
|
|
601
|
+
disabled: !!busy,
|
|
602
|
+
onClick: () => {
|
|
603
|
+
if (isConfirm) { setConfirming(null); runAction("delete", s.id); }
|
|
604
|
+
else { setConfirming(s.id); }
|
|
605
|
+
},
|
|
606
|
+
}, deleting ? "…" : (isConfirm ? "确认删除?" : "删除")));
|
|
607
|
+
return React.createElement(
|
|
608
|
+
"div", { className: "am-row" + (isOrphan ? " am-orphan" : ""), key: s.id, title: titleAttr },
|
|
609
|
+
React.createElement(
|
|
610
|
+
"span", { className: "am-slot", "aria-hidden": true },
|
|
611
|
+
isOrphan ? React.createElement(WarningIcon) : React.createElement(ArchiveIcon)
|
|
612
|
+
),
|
|
613
|
+
React.createElement("span", { className: "am-title" }, isOrphan ? "未知会话(日志已不存在)" : (s.displayTitle || s.title || "未命名会话")),
|
|
614
|
+
React.createElement("span", { className: "am-time" }, timeLabel),
|
|
615
|
+
React.createElement("span", { className: "am-row-actions" }, actions)
|
|
616
|
+
);
|
|
617
|
+
}) : null
|
|
618
|
+
)
|
|
619
|
+
)
|
|
620
|
+
);
|
|
621
|
+
const ghostNotice = ghostIds.length > 0
|
|
622
|
+
? React.createElement("div", { className: "am-ghost" }, ghostIds.length + " 个已删除会话仍驻留内存(已隐藏),重启 DSH 后自动清除")
|
|
623
|
+
: null;
|
|
624
|
+
return React.createElement(
|
|
625
|
+
"div", { className: "am-panel", onClick: (e) => e.stopPropagation() },
|
|
626
|
+
header,
|
|
627
|
+
error ? React.createElement("div", { className: "am-error" }, error) : null,
|
|
628
|
+
ghostNotice,
|
|
629
|
+
search,
|
|
630
|
+
body
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// ---- Modern (DSH >= 0.1.7) delete surfaces ---------------------------
|
|
635
|
+
// The built-in archive management owns listing/restore on 0.1.7+; these
|
|
636
|
+
// three components add only DELETION on top of it. Both entry points
|
|
637
|
+
// show for archived, non-ghost rows only and open the shared confirm
|
|
638
|
+
// dialog (rendered from the sidebar foot slot — see the stacking note
|
|
639
|
+
// on the footer registration below).
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Danger row in an archived session's "..." menu
|
|
643
|
+
* (`sidebar.workspaces.session.menu.item`, order 450 — after the
|
|
644
|
+
* shipped pin/rename/fork/archive rows). Dismisses the menu through the
|
|
645
|
+
* slot's `useMenuOpenState` hook before raising the confirm dialog.
|
|
646
|
+
*
|
|
647
|
+
* @param props - owner share (row identity) + the slot's hook seat + the global workspace hook.
|
|
648
|
+
* @returns the menu row, or null when delete does not apply.
|
|
649
|
+
*/
|
|
650
|
+
function DeleteMenuEntry({ sessionId, displayTitle, useMenuOpenState, useWorkspaces }) {
|
|
651
|
+
const archived = useArchivedMember(useWorkspaces, sessionId);
|
|
652
|
+
const ghostIds = useGhostIds();
|
|
653
|
+
const ghost = ghostIds.indexOf(sessionId) !== -1;
|
|
654
|
+
const menuState = typeof useMenuOpenState === "function" ? useMenuOpenState() : null;
|
|
655
|
+
const setMenuOpen = menuState ? menuState[1] : () => {};
|
|
656
|
+
// Keep the ghost list fresh while the menu offering delete is open.
|
|
657
|
+
React.useEffect(() => { if (archived) refreshState(); }, [archived, sessionId]);
|
|
658
|
+
if (!archived || ghost) return null;
|
|
659
|
+
const select = () => {
|
|
660
|
+
setMenuOpen(false);
|
|
661
|
+
deleteStore.set({ sessionId, displayTitle });
|
|
662
|
+
};
|
|
663
|
+
if (ui && ui.MenuItemButton) {
|
|
664
|
+
return React.createElement(
|
|
665
|
+
ui.MenuItemButton,
|
|
666
|
+
{
|
|
667
|
+
danger: true,
|
|
668
|
+
separatorBefore: true,
|
|
669
|
+
icon: React.createElement(TrashIcon, { size: 14 }),
|
|
670
|
+
onSelect: select,
|
|
671
|
+
},
|
|
672
|
+
"删除会话…"
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
return React.createElement(
|
|
676
|
+
"button",
|
|
677
|
+
{ type: "button", role: "menuitem", className: "am-menu-del", onClick: select },
|
|
678
|
+
React.createElement(TrashIcon, { size: 14 }),
|
|
679
|
+
"删除会话…"
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Hover trash button at the archived row's end
|
|
685
|
+
* (`sidebar.workspaces.session.row.action`, order 300 — after the
|
|
686
|
+
* shipped archive/pin buttons). Clicks inside the row-action strip stay
|
|
687
|
+
* in the strip, so no propagation handling is needed.
|
|
688
|
+
*
|
|
689
|
+
* @param props - owner share (row identity) + the global workspace hook.
|
|
690
|
+
* @returns the button, or null when delete does not apply.
|
|
691
|
+
*/
|
|
692
|
+
function DeleteRowButton({ sessionId, displayTitle, useWorkspaces }) {
|
|
693
|
+
const archived = useArchivedMember(useWorkspaces, sessionId);
|
|
694
|
+
const ghostIds = useGhostIds();
|
|
695
|
+
const ghost = ghostIds.indexOf(sessionId) !== -1;
|
|
696
|
+
React.useEffect(() => { if (archived) refreshState(); }, [archived, sessionId]);
|
|
697
|
+
if (!archived || ghost) return null;
|
|
698
|
+
const button = React.createElement(
|
|
699
|
+
"button",
|
|
700
|
+
{
|
|
701
|
+
type: "button",
|
|
702
|
+
className: "am-row-del",
|
|
703
|
+
title: "删除会话",
|
|
704
|
+
"aria-label": "删除会话",
|
|
705
|
+
onClick: () => deleteStore.set({ sessionId, displayTitle }),
|
|
706
|
+
},
|
|
707
|
+
React.createElement(TrashIcon, { size: 14 })
|
|
708
|
+
);
|
|
709
|
+
if (ui && ui.Tooltip) {
|
|
710
|
+
return React.createElement(
|
|
711
|
+
ui.Tooltip,
|
|
712
|
+
{ label: "删除会话", side: "bottom", align: "end", delayMs: 500 },
|
|
713
|
+
button
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
return button;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* The modern confirm dialog: permanent deletion gets the same 二次确认
|
|
721
|
+
* treatment as the legacy panel (per-request state dies with the
|
|
722
|
+
* request; errors stay visible; a live-but-idle target resolves into a
|
|
723
|
+
* ghost explanation instead of closing silently).
|
|
724
|
+
*
|
|
725
|
+
* @param props - the pending request and its dismissal.
|
|
726
|
+
* @returns the fixed overlay + dialog.
|
|
727
|
+
*/
|
|
728
|
+
function DeleteConfirmForm({ req, onClose }) {
|
|
729
|
+
const [busy, setBusy] = React.useState(false);
|
|
730
|
+
const [error, setError] = React.useState(null);
|
|
731
|
+
const [ghost, setGhost] = React.useState(false);
|
|
732
|
+
// Esc cancels: the dialog owns z-index:1000, so swallow the key to
|
|
733
|
+
// keep it from reaching the shell's global Escape handlers.
|
|
734
|
+
React.useEffect(() => {
|
|
735
|
+
const onKeyDown = (e) => {
|
|
736
|
+
if (e.key === "Escape") {
|
|
737
|
+
e.stopPropagation();
|
|
738
|
+
if (!busy) onClose();
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
742
|
+
return () => document.removeEventListener("keydown", onKeyDown, true);
|
|
743
|
+
}, [busy]);
|
|
744
|
+
const close = () => { if (!busy) onClose(); };
|
|
745
|
+
const confirm = async () => {
|
|
746
|
+
if (busy) return;
|
|
747
|
+
setBusy(true);
|
|
748
|
+
setError(null);
|
|
749
|
+
try {
|
|
750
|
+
const res = await remote.delete({ sessionId: req.sessionId });
|
|
751
|
+
if (res && res.ok) {
|
|
752
|
+
try {
|
|
753
|
+
const sessions = ctx.get("sessions");
|
|
754
|
+
if (sessions && typeof sessions.refresh === "function") sessions.refresh();
|
|
755
|
+
} catch { /* non-fatal */ }
|
|
756
|
+
refreshState();
|
|
757
|
+
if (res.value && res.value.live) {
|
|
758
|
+
// Live-but-idle: files are gone, the archive entry stays as a
|
|
759
|
+
// ghost until restart (same semantics as the legacy panel).
|
|
760
|
+
setGhost(true);
|
|
761
|
+
} else {
|
|
762
|
+
onClose();
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
const err = res && res.error;
|
|
766
|
+
setError((err && err.message) || (err && err.code) || "删除失败");
|
|
767
|
+
}
|
|
768
|
+
} catch (e) {
|
|
769
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
770
|
+
} finally {
|
|
771
|
+
setBusy(false);
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
const title = req.displayTitle || req.sessionId;
|
|
775
|
+
return React.createElement(
|
|
776
|
+
"div", { className: "am-overlay", onClick: close },
|
|
777
|
+
React.createElement(
|
|
778
|
+
"div", { className: "am-confirm", onClick: (e) => e.stopPropagation() },
|
|
779
|
+
React.createElement(
|
|
780
|
+
"div", { className: "am-header" },
|
|
781
|
+
React.createElement("span", { className: "am-title" }, ghost ? "已删除会话" : "删除会话"),
|
|
782
|
+
React.createElement(
|
|
783
|
+
"button",
|
|
784
|
+
{ className: "am-close", onClick: close, disabled: busy, title: "关闭", "aria-label": "关闭" },
|
|
785
|
+
"✕"
|
|
786
|
+
)
|
|
787
|
+
),
|
|
788
|
+
React.createElement(
|
|
789
|
+
"div", { className: "am-confirm-body" },
|
|
790
|
+
ghost
|
|
791
|
+
? "日志文件已删除。该会话仍驻留内存,删除条目将在重启 DSH 后自动清除。"
|
|
792
|
+
: "将永久删除「" + title + "」的会话日志文件,此操作不可恢复。"
|
|
793
|
+
),
|
|
794
|
+
error && !ghost
|
|
795
|
+
? React.createElement("div", { className: "am-confirm-error" }, error)
|
|
796
|
+
: null,
|
|
797
|
+
React.createElement(
|
|
798
|
+
"div", { className: "am-confirm-actions" },
|
|
799
|
+
ghost
|
|
800
|
+
? React.createElement(
|
|
801
|
+
"button",
|
|
802
|
+
{ type: "button", className: "am-confirm-btn", onClick: onClose },
|
|
803
|
+
"关闭"
|
|
804
|
+
)
|
|
805
|
+
: [
|
|
806
|
+
React.createElement(
|
|
807
|
+
"button",
|
|
808
|
+
{ key: "cancel", type: "button", className: "am-confirm-btn", disabled: busy, onClick: close },
|
|
809
|
+
"取消"
|
|
810
|
+
),
|
|
811
|
+
React.createElement(
|
|
812
|
+
"button",
|
|
813
|
+
{ key: "confirm", type: "button", className: "am-confirm-btn am-confirm-danger", disabled: busy, onClick: confirm },
|
|
814
|
+
busy ? "删除中…" : "永久删除"
|
|
815
|
+
),
|
|
816
|
+
]
|
|
817
|
+
)
|
|
818
|
+
)
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Sidebar footer action entry (Cordis Plugin 下方、设置上方;纵向排列由注入
|
|
823
|
+
// CSS 保证) + frame-wide archive manager panel.
|
|
824
|
+
//
|
|
825
|
+
// 弹窗为什么从这里渲染而不是 `shell.overlay`:AppFrame 把 `shell.overlay`
|
|
826
|
+
// 渲染在 `.overlayLayer`(position:absolute; z-index:20)内,该层形成独立
|
|
827
|
+
// stacking context,把子元素 z-index 锁死在其下;而 dsh-better-sidebar 把
|
|
828
|
+
// 整层 append 到 document.body(position:fixed; z-index:25),归档弹窗因此
|
|
829
|
+
// 会被它盖住。设置弹窗(ui-settings)的做法是把全屏 fixed 遮罩
|
|
830
|
+
// (z-index:1000)从 sidebar.settings 槽渲染——侧栏列不构成 stacking
|
|
831
|
+
// context,fixed 层直接参与根 stacking context,压过 better-sidebar 的 25。
|
|
832
|
+
// 这里照搬同一做法:弹窗与按钮同从 sidebar.footer.action 槽渲染,两者共享
|
|
833
|
+
// useOpen 状态,UI 行为不变。
|
|
834
|
+
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register(
|
|
835
|
+
{ name: "sidebar.footer.action", id: "archive-manager", order: 500, label: () => "归档" },
|
|
836
|
+
(props) => {
|
|
837
|
+
const wide = props.wide === true;
|
|
838
|
+
const ghostIds = useGhostIds();
|
|
839
|
+
let rawCount = 0;
|
|
840
|
+
try { rawCount = props.useWorkspaces((s) => s.archivedSessionIds.length) || 0; } catch { rawCount = 0; }
|
|
841
|
+
React.useEffect(() => { refreshState(); }, []);
|
|
842
|
+
const count = rawCount - (ghostIds.length > rawCount ? rawCount : ghostIds.length);
|
|
843
|
+
const safeCount = count > 0 ? count : 0;
|
|
844
|
+
const isOpen = useOpen();
|
|
845
|
+
const modern = useModern();
|
|
846
|
+
const deleteReq = useDeleteRequest();
|
|
847
|
+
// Esc 关闭弹窗:打开期间在 document 捕获阶段监听 keydown,命中
|
|
848
|
+
// Escape 即关闭并 stopPropagation——弹窗位于 z-index:1000 顶层,
|
|
849
|
+
// 此时 Esc 应只作用于它,不穿透到 shell 的全局快捷键或底层输入框。
|
|
850
|
+
React.useEffect(() => {
|
|
851
|
+
if (!isOpen) return undefined;
|
|
852
|
+
const onKeyDown = (e) => {
|
|
853
|
+
if (e.key === "Escape") {
|
|
854
|
+
e.stopPropagation();
|
|
855
|
+
setOpen(false);
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
859
|
+
return () => document.removeEventListener("keydown", onKeyDown, true);
|
|
860
|
+
}, [isOpen]);
|
|
861
|
+
// DSH >= 0.1.7: the shell's built-in archive management owns the
|
|
862
|
+
// sidebar entry — no button, no panel; this slot carries only the
|
|
863
|
+
// delete confirm dialog that this mode exists to add.
|
|
864
|
+
if (modern) {
|
|
865
|
+
return deleteReq
|
|
866
|
+
? React.createElement(DeleteConfirmForm, {
|
|
867
|
+
key: deleteReq.sessionId,
|
|
868
|
+
req: deleteReq,
|
|
869
|
+
onClose: () => deleteStore.set(null),
|
|
870
|
+
})
|
|
871
|
+
: null;
|
|
872
|
+
}
|
|
873
|
+
const children = [];
|
|
874
|
+
children.push(React.createElement("span", { key: "icon", className: "am-foot-icon", "aria-hidden": true }, React.createElement(ArchiveIcon)));
|
|
875
|
+
if (wide) children.push(React.createElement("span", { key: "label", className: "am-foot-label" }, "归档"));
|
|
876
|
+
if (wide && safeCount > 0) children.push(React.createElement("span", { key: "cnt", className: "am-badge" }, String(safeCount)));
|
|
877
|
+
const button = React.createElement(
|
|
878
|
+
"div", { className: "am-foot-layer" + (wide ? "" : " am-foot-rail") },
|
|
879
|
+
React.createElement("button", {
|
|
880
|
+
type: "button",
|
|
881
|
+
onClick: () => { refreshState(); setOpen(!getOpen()); },
|
|
882
|
+
title: "归档管理",
|
|
883
|
+
"aria-label": "归档管理",
|
|
884
|
+
className: "am-foot" + (isOpen ? " am-active" : ""),
|
|
885
|
+
}, children)
|
|
886
|
+
);
|
|
887
|
+
const overlay = isOpen
|
|
888
|
+
? React.createElement(
|
|
889
|
+
"div", { className: "am-overlay", onClick: () => setOpen(false) },
|
|
890
|
+
React.createElement(ArchiveManager, { slotProps: props, onClose: () => setOpen(false) })
|
|
891
|
+
)
|
|
892
|
+
: null;
|
|
893
|
+
return [button, overlay];
|
|
894
|
+
}
|
|
895
|
+
));
|
|
896
|
+
|
|
897
|
+
// ---- Modern-mode registrations (fired only on DSH >= 0.1.7) ----------
|
|
898
|
+
// ui-workspace declares these two lists when it registers the built-in
|
|
899
|
+
// archive browsing region (the "仅显示已归档" filter world). On earlier
|
|
900
|
+
// DSH builds the keys are never declared: ctx.slots.inject() waits
|
|
901
|
+
// silently, these callbacks never run, no entry is registered, and the
|
|
902
|
+
// legacy footer button stays — that IS the version switch. The first
|
|
903
|
+
// declaration also flips markModern(), which hides the legacy button
|
|
904
|
+
// and drops the footer-column CSS for good.
|
|
905
|
+
ctx.slots.inject("sidebar.workspaces.session.menu.item", () => {
|
|
906
|
+
markModern();
|
|
907
|
+
return ctx.slots.register(
|
|
908
|
+
{ name: "sidebar.workspaces.session.menu.item", id: "dsh-archive-manager.delete", order: 450 },
|
|
909
|
+
DeleteMenuEntry
|
|
910
|
+
);
|
|
911
|
+
});
|
|
912
|
+
ctx.slots.inject("sidebar.workspaces.session.row.action", () => {
|
|
913
|
+
markModern();
|
|
914
|
+
return ctx.slots.register(
|
|
915
|
+
{ name: "sidebar.workspaces.session.row.action", id: "dsh-archive-manager.delete", order: 300 },
|
|
916
|
+
DeleteRowButton
|
|
917
|
+
);
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
exports.apply = apply;
|
|
922
|
+
exports.inject = ["slots", "remote"];
|
|
923
|
+
// Exposed for verify-typert-compat.mjs (the browser wiring contract has
|
|
924
|
+
// to satisfy both DSH typert registries; nothing else reads this).
|
|
925
|
+
exports.CLIENT_REMOTE = CLIENT_REMOTE;
|
|
926
|
+
return module.exports;
|
|
927
|
+
},
|
|
928
|
+
});
|