dsh-session-manager 0.4.11 → 0.5.2
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/CHANGELOG.md +60 -2
- package/README.md +73 -44
- package/README.zh.md +137 -112
- package/lib/annotation-store.js +199 -0
- package/lib/client.js +2772 -1196
- package/lib/clipboard-parser.js +208 -0
- package/lib/compat/dsh-adapter.js +17 -0
- package/lib/compat/zstd-frames.js +2 -2
- package/lib/index.js +1243 -1247
- package/lib/session-files.js +193 -0
- package/package.json +10 -2
package/lib/client.js
CHANGED
|
@@ -1,1196 +1,2772 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @dsh-session-manager — Client half.
|
|
3
|
-
*
|
|
4
|
-
* Renders the title-bar action cluster (Archive / Unarchive, Move to
|
|
5
|
-
* workspace, Delete session) and the sidebar-foot Session manager panel
|
|
6
|
-
* (Open / Archive / Unarchive / Move / Migrate preset / Delete per row,
|
|
7
|
-
* plus
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
var
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
window.
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// the
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
host.__smRoot.
|
|
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
|
-
".sm-panelDialog{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);width:560px;max-width:calc(100vw - 32px);max-height:540px;height:auto;box-sizing:border-box;display:flex;flex-direction:column;gap:0;background:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.25);overflow:hidden;z-index:9999;opacity:1}.sm-panelDialog .sm-nativeDialogHeader{padding:10px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#eee);flex:none}.sm-panelDialog .sm-nativeDialogTitle{margin:0;font-size:14px;line-height:20px;font-weight:600}.sm-panelDialog .sm-nativeDialogFooter{padding:10px 16px;border-top:1px solid var(--dsw-alias-border-l2,#eee);flex:none;flex-wrap:wrap;justify-content:flex-end;gap:6px;margin-top:0}.sm-panelDialog .sm-panel{display:flex;flex-direction:column;gap:6px;min-height:0;flex:1 1 auto;overflow:auto;padding:8px 12px}.sm-panelDialog .sm-list{display:flex;flex-direction:column;gap:2px;flex:1 1 auto;min-height:0;overflow:auto}.sm-panelDialog .sm-row{display:flex;align-items:center;gap:8px;padding:0 10px;height:36px;min-height:36px;border:1px solid transparent;border-radius:8px;background:transparent}.sm-panelDialog .sm-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.sm-panelDialog .sm-rowCurrent{background:var(--dsw-alias-fill-l2);border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-panelDialog .sm-rowMain{display:flex;align-items:stretch!important;gap:8px;min-width:0;flex:1 1 auto;overflow:hidden;text-align:left!important}.sm-panelDialog .sm-rowTitle{min-width:0;flex:1 1 0%;width:100%;align-self:stretch;display:flex;align-items:center;justify-content:flex-start;gap:6px;font-size:12px;line-height:16px;overflow:hidden;text-align:left!important;direction:ltr!important}.sm-confirmDialog{z-index:10000}.sm-confirmDialog.sm-nativeDialogLayer{position:fixed;left:calc(50vw + 184px);top:calc(50vh - 90px);width:300px;max-width:calc(100vw - 32px);max-height:none;display:block;align-items:initial;justify-content:initial;box-sizing:border-box;background:transparent;border:0;box-shadow:none;padding:0;inset:calc(50vh - 90px) auto auto calc(50vw + 308px)}.sm-confirmDialog.sm-nativeDialogLayer .sm-nativeDialogBackdrop{display:none}.sm-confirmDialog.sm-nativeDialogLayer .sm-nativeDialog{position:relative;left:auto;top:auto;transform:none;width:auto;box-sizing:border-box;background:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.25);padding:0;overflow:hidden;animation:sm-confirmPop .14s ease-out}.sm-confirmDialog .sm-nativeDialogHeader{padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#eee)}.sm-confirmDialog .sm-nativeDialogTitle{margin:0;font-size:14px;line-height:20px;font-weight:600}.sm-confirmDialog .sm-nativeDialogBody{padding:10px 16px;display:flex;flex-direction:column;gap:8px;min-height:0}.sm-confirmDialog .sm-nativeDialogDescription{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap}.sm-confirmDialog .sm-warn{padding:6px 10px;font-size:12px;line-height:16px;margin-top:0}.sm-confirmDialog .sm-nativeDialogFooter{padding:10px 16px;border-top:1px solid var(--dsw-alias-border-l2,#eee);flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:0}.sm-confirmDialog .sm-nativeDialogButton{min-height:28px;padding:3px 14px;font-size:12px;line-height:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;background:transparent;color:var(--dsw-alias-label-primary)}.sm-confirmDialog .sm-nativeDialogCancel:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.sm-confirmDialog .sm-nativeDialogConfirm{background:var(--dsw-alias-state-error-primary);color:#fff;border-color:var(--dsw-alias-state-error-primary)}.sm-confirmDialog .sm-nativeDialogConfirm:hover:not(:disabled){filter:brightness(.94)}@keyframes sm-confirmPop{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.sm-confirmDialog.sm-nativeDialog{padding:0;width:auto;max-width:none}.sm-panelDialog .sm-rowTitle>span:first-child{display:block;min-width:0;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;flex:1 1 auto;width:auto;text-align:left!important;direction:ltr!important;unicode-bidi:plaintext}.sm-panelDialog .sm-rowMeta{display:none}.sm-panelDialog .sm-rowActions{display:flex;gap:2px;flex:none;justify-content:flex-end}.sm-panelDialog .sm-rowActions .sm-rowBtn{min-height:22px;height:22px;padding:0 8px;font-size:11px;line-height:14px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}.sm-panelDialog .sm-rowActions .sm-rowBtn:hover{background:var(--dsw-alias-fill-l2);color:var(--dsw-alias-label-primary)}.sm-panelDialog .sm-rowActions .sm-rowBtnDanger{color:var(--dsw-alias-state-error-primary)}.sm-panelDialog .sm-filter{flex:none;padding:6px 12px 4px;gap:4px;display:flex;flex-wrap:wrap}.sm-panelDialog .sm-filterBtn{min-height:24px;padding:2px 10px;font-size:11px;line-height:16px;border-radius:4px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}.sm-panelDialog .sm-filterBtn.sm-on{background:var(--dsw-alias-fill-l2);color:var(--dsw-alias-label-primary)}.sm-panelDialog .sm-badge{font-size:10px;line-height:14px;padding:0 5px;border-radius:6px;flex:none}.sm-panelDialog .sm-error{padding:6px 10px;margin:0;font-size:12px;line-height:16px}.sm-panelDialog .sm-nativeDialogButton{min-height:28px;padding:3px 12px;font-size:12px;line-height:18px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);cursor:pointer}.sm-panelDialog .sm-nativeDialogCancel{background:transparent}.sm-panelDialog .sm-nativeDialogGhost{background:transparent;border-color:var(--dsw-alias-border-l2)}.sm-panelDialog .sm-nativeDialogConfirm{background:var(--dsw-alias-accent-primary,#356ae6);color:#fff;border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-panelDialog .sm-empty{padding:18px 0;font-size:12px;color:var(--dsw-alias-label-tertiary);text-align:center}.sm-panelDialog .sm-footError{font-size:11px;color:var(--dsw-alias-state-error-primary);margin-right:auto;line-height:20px}.sm-panelDialog .sm-moveLabel{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}.sm-migrateDialog .sm-nativeDialogFooter{flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:14px}.sm-migrateDialog .sm-migrateChip{cursor:pointer;pointer-events:auto;position:relative;z-index:1}.sm-migrateDialog .sm-migrateChip[disabled]{opacity:.5;cursor:not-allowed}.sm-migrateDialog.sm-nativeDialogLayer{position:fixed;left:50vw;top:auto;bottom:48px;transform:translateX(-50%);width:min(560px,calc(100vw - 32px));max-height:none;display:block;align-items:initial;justify-content:initial;box-sizing:border-box;background:transparent;border:0;box-shadow:none;padding:0;inset:auto;z-index:10001}.sm-migrateDialog.sm-nativeDialogLayer .sm-nativeDialogBackdrop{display:none}.sm-migrateDialog.sm-nativeDialogLayer .sm-nativeDialog{position:relative;left:auto;top:auto;transform:none;width:auto;max-height:min(70vh,520px);box-sizing:border-box;background-color:#ffffff;background-color:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.45);overflow:hidden;padding:0;display:flex;flex-direction:column;gap:0;opacity:1}.sm-migrateDialog{position:fixed;left:50vw;top:auto;bottom:48px;transform:translateX(-50%);width:min(560px,calc(100vw - 32px));max-height:min(70vh,520px);box-sizing:border-box;display:flex;flex-direction:column;gap:0;background-color:#ffffff;background-color:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.45);overflow:hidden;opacity:1;z-index:10001;backdrop-filter:none;-webkit-backdrop-filter:none}.sm-migrateDialog .sm-nativeDialogHeader{padding:12px 18px;border-bottom:1px solid #eee;border-bottom:1px solid var(--dsw-alias-border-l2,#eee);flex:none;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-nativeDialogTitle{margin:0;font-size:15px;line-height:22px;font-weight:600;color:#111;color:var(--dsw-alias-label-primary,#111)}.sm-migrateDialog .sm-migratePanel{padding:12px 18px;display:flex;flex-direction:column;gap:14px;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-nativeDialogBody{padding:0;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-nativeDialogFooter{padding:12px 18px;border-top:1px solid #eee;border-top:1px solid var(--dsw-alias-border-l2,#eee);flex:none;flex-wrap:wrap;justify-content:flex-end;gap:8px;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-migrateRow>label{color:#444;color:var(--dsw-alias-label-secondary,#444)}.sm-migrateDialog .sm-migrateChip{padding:6px 12px;border-radius:999px;border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);background-color:#f5f5f5;background-color:var(--dsw-alias-fill-l2,#f5f5f5);color:#111;color:var(--dsw-alias-label-primary,#111);font-size:13px;line-height:18px;cursor:pointer;pointer-events:auto;position:relative;z-index:1;white-space:nowrap}.sm-migrateDialog .sm-migrateChip:hover:not(:disabled){background-color:#e8e8e8;background-color:var(--dsw-alias-interactive-bg-hover,#e8e8e8)}.sm-migrateDialog .sm-migrateChipOn{background-color:#356ae6;background-color:var(--dsw-alias-accent-primary,#356ae6);color:#fff;border-color:#356ae6;border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-migrateDialog .sm-migrateChip.sm-migrateChipOn:hover:not(:disabled){background-color:#4576f0;background-color:var(--dsw-alias-accent-primary-hover,#4576f0);color:#fff;border-color:#4576f0;border-color:var(--dsw-alias-accent-primary-hover,#4576f0);filter:none}.sm-migrateDialog .sm-migrateChipOn:hover:not(:disabled){filter:brightness(.92)}.sm-migrateDialog .sm-migrateChip[disabled]{opacity:.5;cursor:not-allowed}.sm-migrateDialog .sm-migrateEmpty{color:#888;color:var(--dsw-alias-label-tertiary,#888)}.sm-migrateDialog .sm-migratePreview{padding:10px 12px;border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;background-color:#f5f5f5;background-color:var(--dsw-alias-fill-l2,#f5f5f5);color:#111;color:var(--dsw-alias-label-primary,#111);font-size:13px;line-height:18px}.sm-migrateDialog .sm-migrateWarn{padding:10px 12px;border:1px solid rgba(217,119,6,.25);border-radius:8px;background-color:rgba(217,119,6,.08);color:#444;color:var(--dsw-alias-label-secondary,#444);font-size:13px;line-height:18px}.sm-migrateDialog .sm-nativeDialogButton{min-height:32px;padding:4px 14px;font-size:13px;line-height:20px;border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;cursor:pointer;background-color:#fff;background-color:var(--dsw-alias-fill-l2,#fff);color:#111;color:var(--dsw-alias-label-primary,#111)}.sm-migrateDialog .sm-nativeDialogButton:hover:not(:disabled){background-color:#e8e8e8;background-color:var(--dsw-alias-interactive-bg-hover,#e8e8e8)}.sm-migrateDialog .sm-nativeDialogConfirm{background-color:#356ae6;background-color:var(--dsw-alias-accent-primary,#356ae6);color:#fff;border-color:#356ae6;border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-migrateDialog .sm-nativeDialogConfirm:hover:not(:disabled){filter:brightness(.94)}",
|
|
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
|
-
window.addEventListener("keydown", handler);
|
|
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
|
-
const
|
|
698
|
-
|
|
699
|
-
const
|
|
700
|
-
const
|
|
701
|
-
const
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
if (
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
const
|
|
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
|
-
const
|
|
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
|
-
const
|
|
949
|
-
const
|
|
950
|
-
const
|
|
951
|
-
|
|
952
|
-
const
|
|
953
|
-
const
|
|
954
|
-
const
|
|
955
|
-
const
|
|
956
|
-
const
|
|
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
|
-
const
|
|
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
|
-
if (
|
|
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
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @dsh-session-manager — Client half.
|
|
3
|
+
*
|
|
4
|
+
* Renders the title-bar action cluster (Archive / Unarchive, Move to
|
|
5
|
+
* workspace, Delete session) and the sidebar-foot Session manager panel
|
|
6
|
+
* (Open / Archive / Unarchive / Move / Migrate preset / Delete per row,
|
|
7
|
+
* plus search, workspace/archive/annotation filters, sorting and toggle-close behaviour).
|
|
8
|
+
* Favorites, review-later flags, tags, notes and priority share a separate metadata store. No build
|
|
9
|
+
* step: DSH loads the file as a Cordis client bundle via its inject
|
|
10
|
+
* declaration in package.json.
|
|
11
|
+
*/
|
|
12
|
+
window.__ModuleLoader__.load({
|
|
13
|
+
id: "dsh-session-manager",
|
|
14
|
+
factory: (require) => {
|
|
15
|
+
var module = { exports: {} };
|
|
16
|
+
var exports = module.exports;
|
|
17
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
18
|
+
|
|
19
|
+
const React = require("react");
|
|
20
|
+
const { useState, useEffect, useMemo, useRef, useSyncExternalStore } = React;
|
|
21
|
+
const P = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
22
|
+
if (typeof window !== "undefined") {
|
|
23
|
+
window.__smReact = React;
|
|
24
|
+
window.__smReactDOM = React.createPortal ? React : null;
|
|
25
|
+
// Tiny imperative portal helper: takes a React element + a host
|
|
26
|
+
// DOM node, creates a React root on the host (lazily), and re-renders
|
|
27
|
+
// the element into it on each call. Used by ConfirmDialog to escape
|
|
28
|
+
// the sidebar overflow:hidden ancestor.
|
|
29
|
+
try {
|
|
30
|
+
const RDOM = require("react-dom/client");
|
|
31
|
+
if (RDOM && typeof RDOM.createRoot === "function") {
|
|
32
|
+
window.__smRenderInto = (tree, host) => {
|
|
33
|
+
if (!tree || !host) return;
|
|
34
|
+
try {
|
|
35
|
+
if (!host.__smRoot) host.__smRoot = RDOM.createRoot(host);
|
|
36
|
+
host.__smRoot.render(tree);
|
|
37
|
+
} catch (_) { /* ignore render failure */ }
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
} catch (_) { /* react-dom/client not available */ }
|
|
41
|
+
}
|
|
42
|
+
// Monotonically-increasing z-index for popup dialogs so the most-recently
|
|
43
|
+
// opened popup always sits on top of any previously opened popup. The
|
|
44
|
+
// CSS-defined z-index values (panel 9999 / confirm 10000 / annotation +
|
|
45
|
+
// bulk 10002) are static; without a counter the user can open an
|
|
46
|
+
// annotation popup (10002) after a confirm popup (10000) and the older
|
|
47
|
+
// confirm stays below. Each dialog component claims a fresh slot from
|
|
48
|
+
// this counter on mount, writes it to inline style.zIndex, and never
|
|
49
|
+
// recycles within a session, so the most-recently opened popup is
|
|
50
|
+
// always the topmost one.
|
|
51
|
+
let dialogZStackCounter = 11000;
|
|
52
|
+
function nextDialogZ() { return ++dialogZStackCounter; }
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
const NS = "session-manager";
|
|
56
|
+
const API = "/session-manager/api";
|
|
57
|
+
|
|
58
|
+
const zh = {
|
|
59
|
+
"marks.filter.favorite": "收藏",
|
|
60
|
+
"marks.filter.favorite.aria": "收藏筛选",
|
|
61
|
+
"marks.filter.review": "待回看",
|
|
62
|
+
"marks.filter.review.aria": "待回看筛选",
|
|
63
|
+
"marks.filter.tags": "按标签筛选",
|
|
64
|
+
"marks.filter.priority": "按优先级筛选",
|
|
65
|
+
"marks.favorite.add": "收藏会话",
|
|
66
|
+
"marks.favorite.remove": "取消收藏",
|
|
67
|
+
"marks.review.add": "标记待回看",
|
|
68
|
+
"marks.review.remove": "取消待回看",
|
|
69
|
+
"marks.edit": "标签/备注",
|
|
70
|
+
"marks.header.edit": "标签/备注",
|
|
71
|
+
"marks.edit.aria": "编辑标签、备注和优先级",
|
|
72
|
+
"marks.title": "会话标记",
|
|
73
|
+
"marks.favorite": "收藏",
|
|
74
|
+
"marks.review": "待回看",
|
|
75
|
+
"marks.tags": "标签",
|
|
76
|
+
"marks.tags.placeholder": "用逗号分隔,最多 20 个标签,每个 32 字符(重复自动合并)。例如:排障,方案",
|
|
77
|
+
"marks.tags.help": "最多 20 个标签,每个 32 字符;重复标签会自动合并",
|
|
78
|
+
"marks.note": "备注",
|
|
79
|
+
"marks.note.placeholder": "例如:等待 Windows 环境验证;下次继续检查回滚逻辑(最多 2000 字符)",
|
|
80
|
+
"marks.privacy": "仅用于会话整理,不写入聊天记录,也不会自动发送给模型。",
|
|
81
|
+
"marks.priority": "优先级",
|
|
82
|
+
"marks.priority.all": "全部优先级",
|
|
83
|
+
"marks.priority.1": "1 · 最高",
|
|
84
|
+
"marks.priority.2": "2 · 高",
|
|
85
|
+
"marks.priority.3": "3 · 普通",
|
|
86
|
+
"marks.priority.4": "4 · 低",
|
|
87
|
+
"marks.priority.5": "5 · 最低",
|
|
88
|
+
"marks.priority.help": "1 最高,5 最低,默认 3(普通)",
|
|
89
|
+
"marks.tags.all": "全部标签",
|
|
90
|
+
"marks.tags.none": "无标签",
|
|
91
|
+
"marks.tags.missing": "{tag}(无匹配)",
|
|
92
|
+
"marks.save": "保存标记",
|
|
93
|
+
"marks.saving": "保存中…",
|
|
94
|
+
"marks.cancel": "取消",
|
|
95
|
+
"marks.loading": "正在加载会话标记…",
|
|
96
|
+
"marks.error": "会话标记加载失败:{message}",
|
|
97
|
+
"marks.retry": "重试标记加载",
|
|
98
|
+
"marks.conflict": "标记已在其他窗口更新。当前输入仍保留;请载入最新内容后再编辑。",
|
|
99
|
+
"marks.reload": "载入最新内容",
|
|
100
|
+
"marks.discard": "放弃当前未保存的标记修改?",
|
|
101
|
+
"marks.note.exists": "有备注",
|
|
102
|
+
"marks.sort": "优先级(1 → 5)",
|
|
103
|
+
"marks.note.exists": "有备注",
|
|
104
|
+
"marks.import.copy": "复制 Prompt",
|
|
105
|
+
"marks.import.copy.copied": "已复制,请粘贴到当前对话生成结果",
|
|
106
|
+
"marks.import.copy.failed": "复制失败:{message}",
|
|
107
|
+
"marks.import.paste": "导入",
|
|
108
|
+
"marks.import.paste.label": "AI 返回结果",
|
|
109
|
+
"marks.import.paste.help": "把模型返回的 JSON 粘贴到下方输入框,再点击“导入”",
|
|
110
|
+
"marks.import.paste.aria": "AI 返回的 JSON",
|
|
111
|
+
"marks.import.paste.placeholder": "支持 json 代码块或纯 JSON,例如: { \"tags\": [\"标签1\", \"标签2\"], \"note\": \"会话备注\" }",
|
|
112
|
+
"marks.import.paste.clear": "清空输入框",
|
|
113
|
+
"marks.import.empty": "请先把 AI 返回的 JSON 粘贴到上方输入框",
|
|
114
|
+
"marks.import.unknown": "未知字段",
|
|
115
|
+
"marks.import.discard": "导入将覆盖当前未保存的修改,是否继续?",
|
|
116
|
+
"marks.import.failure": "导入失败:{message}",
|
|
117
|
+
"marks.import.warnings": "已导入,但有以下调整:",
|
|
118
|
+
"marks.import.success": "已从剪贴板导入 AI 结果",
|
|
119
|
+
"marks.tags.clear": "清空标签",
|
|
120
|
+
"marks.note.clear": "清空备注",
|
|
121
|
+
"header.aria": "会话操作",
|
|
122
|
+
"menu.open": "打开",
|
|
123
|
+
"menu.archive": "归档会话",
|
|
124
|
+
"menu.unarchive": "移出归档",
|
|
125
|
+
"menu.delete": "删除会话…",
|
|
126
|
+
"menu.move": "移动到工作区…",
|
|
127
|
+
"menu.manage": "打开会话管理…",
|
|
128
|
+
"footer.aria": "会话管理",
|
|
129
|
+
"footer.label": "会话管理",
|
|
130
|
+
"panel.title": "会话管理",
|
|
131
|
+
"panel.github.title": "欢迎Star和Issue",
|
|
132
|
+
"panel.search": "搜索标题、ID、标签或备注",
|
|
133
|
+
"panel.workspace": "工作区",
|
|
134
|
+
"panel.workspace.all": "全部工作区",
|
|
135
|
+
"panel.workspace.ungrouped": "未分组",
|
|
136
|
+
"panel.workspace.loading": "正在加载工作区…",
|
|
137
|
+
"panel.workspace.error": "工作区加载失败:{message}",
|
|
138
|
+
"panel.workspace.retry": "重试",
|
|
139
|
+
"panel.sort": "排序",
|
|
140
|
+
"panel.sort.unavailable": "当前 DSH 未提供创建时间,暂按原顺序显示",
|
|
141
|
+
"panel.sort.updated-desc": "最近更新",
|
|
142
|
+
"panel.sort.updated-asc": "最早更新",
|
|
143
|
+
"panel.sort.created-desc": "最新创建",
|
|
144
|
+
"panel.sort.created-asc": "最早创建",
|
|
145
|
+
"panel.count": "显示 {shown} / {total} 个会话",
|
|
146
|
+
"panel.reset": "重置筛选",
|
|
147
|
+
"panel.empty.filtered": "没有匹配的会话,请调整搜索或筛选条件",
|
|
148
|
+
"panel.filter.all": "全部",
|
|
149
|
+
"panel.filter.active": "未归档",
|
|
150
|
+
"panel.filter.archived": "已归档",
|
|
151
|
+
"panel.empty": "暂无会话",
|
|
152
|
+
"panel.empty.archived": "归档为空",
|
|
153
|
+
"panel.archived": "已归档",
|
|
154
|
+
"panel.running": "运行中",
|
|
155
|
+
"panel.current": "当前",
|
|
156
|
+
"panel.session.new": "新建会话",
|
|
157
|
+
"panel.close": "关闭",
|
|
158
|
+
"row.open": "打开",
|
|
159
|
+
"row.archive": "归档",
|
|
160
|
+
"row.unarchive": "移出归档",
|
|
161
|
+
"row.move": "移动至工作区",
|
|
162
|
+
"row.delete": "删除会话",
|
|
163
|
+
"row.migrate": "迁移预设",
|
|
164
|
+
"migrate.title": "迁移 Agent 预设",
|
|
165
|
+
"migrate.current": "当前预设",
|
|
166
|
+
"migrate.target": "目标预设",
|
|
167
|
+
"migrate.noPresets": "没有可用的目标预设",
|
|
168
|
+
"migrate.same": "请选择与当前预设不同的目标预设",
|
|
169
|
+
"migrate.confirm": "确认迁移",
|
|
170
|
+
"migrate.completed": "Agent 预设已迁移为 {preset}",
|
|
171
|
+
"confirm.delete.title": "删除会话",
|
|
172
|
+
"confirm.delete.desc": "会话「{title}」将被永久删除,包括其全部消息记录与磁盘文件,此操作不可撤销。",
|
|
173
|
+
"confirm.delete.running": "该会话正在运行,删除将立即中断它。",
|
|
174
|
+
"confirm.delete.confirm": "确认删除",
|
|
175
|
+
"confirm.move.title": "移动会话到工作区",
|
|
176
|
+
"confirm.move.desc": "将会话「{title}」移动到目标工作区。",
|
|
177
|
+
"confirm.move.running": "该会话正在运行,移动会中断并关闭它;之后可在目标工作区重新打开继续。",
|
|
178
|
+
"confirm.move.empty": "没有可移动到的其他工作区。",
|
|
179
|
+
"confirm.move.select": "选择目标工作区",
|
|
180
|
+
"confirm.move.current": "当前工作区:{name}",
|
|
181
|
+
"confirm.move.confirm": "确认移动",
|
|
182
|
+
"confirm.cancel": "取消",
|
|
183
|
+
"busy.processing": "处理中…",
|
|
184
|
+
"error.operation": "操作失败:{message}",
|
|
185
|
+
"workspace.default": "默认工作区",
|
|
186
|
+
"workspace.sessions": "{count} 个会话",
|
|
187
|
+
"bulk.title": "批量管理",
|
|
188
|
+
"bulk.selectedFiltered": "已选 {selected} / {filtered} 项",
|
|
189
|
+
"bulk.selectAllFiltered": "全选当前筛选",
|
|
190
|
+
"bulk.clear": "清空选择",
|
|
191
|
+
"bulk.archive": "归档",
|
|
192
|
+
"bulk.unarchive": "取消归档",
|
|
193
|
+
"bulk.delete": "删除会话",
|
|
194
|
+
"bulk.move": "移动至工作区",
|
|
195
|
+
"bulk.moveBtn": "移动至工作区",
|
|
196
|
+
"bulk.presetMigrate": "迁移预设",
|
|
197
|
+
"bulk.favorite": "收藏",
|
|
198
|
+
"bulk.unfavorite": "取消收藏",
|
|
199
|
+
"bulk.review": "待回看",
|
|
200
|
+
"bulk.unreview": "取消待回看",
|
|
201
|
+
"bulk.tagsAdd": "添加标签",
|
|
202
|
+
"bulk.tagsRemove": "清空标签",
|
|
203
|
+
"bulk.priority": "设置优先级",
|
|
204
|
+
"bulk.tags.placeholder": "用逗号分隔,例如:排障, 方案",
|
|
205
|
+
"bulk.priority.aria": "选择优先级",
|
|
206
|
+
"bulk.workspace.aria": "选择目标工作区",
|
|
207
|
+
"bulk.preset.aria": "选择目标 preset",
|
|
208
|
+
"bulk.dialog.title": "批量操作:{action}",
|
|
209
|
+
"bulk.dialog.summary": "将会对 {count} 个会话执行「{action}」。",
|
|
210
|
+
"bulk.dialog.warning.delete": "删除操作不可撤销,请确认后再继续。",
|
|
211
|
+
"bulk.confirm.archive": "确认归档",
|
|
212
|
+
"bulk.confirm.unarchive": "确认还原",
|
|
213
|
+
"bulk.confirm.favorite": "确认收藏",
|
|
214
|
+
"bulk.confirm.unfavorite": "确认移除",
|
|
215
|
+
"bulk.confirm.review": "确认标记",
|
|
216
|
+
"bulk.confirm.unreview": "确认清除",
|
|
217
|
+
"bulk.confirm.tagsAdd": "确认添加",
|
|
218
|
+
"bulk.confirm.tagsRemove": "确认清空",
|
|
219
|
+
"bulk.confirm.priority": "确认设置",
|
|
220
|
+
"bulk.confirm.move": "确认移动",
|
|
221
|
+
"bulk.confirm.presetMigrate": "确认迁移",
|
|
222
|
+
"bulk.confirm.delete": "确认删除",
|
|
223
|
+
"bulk.dialog.execute": "执行",
|
|
224
|
+
"bulk.dialog.cancel": "取消",
|
|
225
|
+
"bulk.progress.title": "正在执行:{action}",
|
|
226
|
+
"bulk.progress.current": "处理 {done} / {total}",
|
|
227
|
+
"bulk.progress.cancel": "取消",
|
|
228
|
+
"bulk.progress.completed": "已完成 {done} / {total}",
|
|
229
|
+
"bulk.result.title": "批量操作完成:{action}",
|
|
230
|
+
"bulk.result.successGroup": "成功 ({count})",
|
|
231
|
+
"bulk.result.failedGroup": "失败 ({count})",
|
|
232
|
+
"bulk.result.skippedGroup": "跳过 ({count})",
|
|
233
|
+
"bulk.result.retry": "重试失败项",
|
|
234
|
+
"bulk.result.close": "关闭",
|
|
235
|
+
"bulk.error.tooMany": "批量操作最多支持 200 项,请缩小筛选范围后重试",
|
|
236
|
+
"bulk.checkbox.aria": "选择 {title}",
|
|
237
|
+
"bulk.heading": "批量操作",
|
|
238
|
+
"panel.selectMode": "批量处理",
|
|
239
|
+
"panel.selectMode.exit": "退出批量处理",
|
|
240
|
+
"panel.selectMode.aria": "进入批量管理模式",
|
|
241
|
+
"panel.selectMode.active.aria": "退出批量管理模式",
|
|
242
|
+
};
|
|
243
|
+
const en = {
|
|
244
|
+
"marks.filter.favorite": "Favorites",
|
|
245
|
+
"marks.filter.favorite.aria": "Favorites filter",
|
|
246
|
+
"marks.filter.review": "Review later",
|
|
247
|
+
"marks.filter.review.aria": "Review later filter",
|
|
248
|
+
"marks.filter.tags": "Filter by tag",
|
|
249
|
+
"marks.filter.priority": "Filter by priority",
|
|
250
|
+
"marks.favorite.add": "Favorite session",
|
|
251
|
+
"marks.favorite.remove": "Remove favorite",
|
|
252
|
+
"marks.review.add": "Mark for review",
|
|
253
|
+
"marks.review.remove": "Clear review flag",
|
|
254
|
+
"marks.edit": "Tags/Notes",
|
|
255
|
+
"marks.header.edit": "Tags/Notes",
|
|
256
|
+
"marks.edit.aria": "Edit tags, notes and priority",
|
|
257
|
+
"marks.title": "Session annotations",
|
|
258
|
+
"marks.favorite": "Favorite",
|
|
259
|
+
"marks.review": "Review later",
|
|
260
|
+
"marks.tags": "Tags",
|
|
261
|
+
"marks.tags.placeholder": "Separate with commas. Up to 20 tags, 32 chars each (duplicates merged). e.g. bug, design",
|
|
262
|
+
"marks.tags.help": "Up to 20 tags, 32 characters each; duplicates are merged",
|
|
263
|
+
"marks.note": "Note",
|
|
264
|
+
"marks.note.placeholder": "For example: awaiting Windows verification; check rollback next (up to 2000 characters)",
|
|
265
|
+
"marks.privacy": "For organization only. Not written into chat history or automatically sent to the model.",
|
|
266
|
+
"marks.priority": "Priority",
|
|
267
|
+
"marks.priority.all": "All priorities",
|
|
268
|
+
"marks.priority.1": "1 · Highest",
|
|
269
|
+
"marks.priority.2": "2 · High",
|
|
270
|
+
"marks.priority.3": "3 · Normal",
|
|
271
|
+
"marks.priority.4": "4 · Low",
|
|
272
|
+
"marks.priority.5": "5 · Lowest",
|
|
273
|
+
"marks.priority.help": "1 is highest, 5 is lowest. Default is 3 (Normal).",
|
|
274
|
+
"marks.tags.all": "All tags",
|
|
275
|
+
"marks.tags.none": "No tags",
|
|
276
|
+
"marks.tags.missing": "{tag} (no matches)",
|
|
277
|
+
"marks.save": "Save annotations",
|
|
278
|
+
"marks.saving": "Saving…",
|
|
279
|
+
"marks.cancel": "Cancel",
|
|
280
|
+
"marks.loading": "Loading session annotations…",
|
|
281
|
+
"marks.error": "Could not load annotations: {message}",
|
|
282
|
+
"marks.retry": "Retry annotations",
|
|
283
|
+
"marks.conflict": "Another window changed these annotations. Your draft is preserved; load the latest version before editing again.",
|
|
284
|
+
"marks.reload": "Load latest",
|
|
285
|
+
"marks.discard": "Discard unsaved annotation changes?",
|
|
286
|
+
"marks.note.exists": "Has note",
|
|
287
|
+
"marks.sort": "Priority (1 → 5)",
|
|
288
|
+
"marks.note.exists": "Has note",
|
|
289
|
+
"marks.import.copy": "Copy Prompt",
|
|
290
|
+
"marks.import.copy.copied": "Copied. Paste it into the current conversation to generate the result.",
|
|
291
|
+
"marks.import.copy.failed": "Copy failed: {message}",
|
|
292
|
+
"marks.import.paste": "Import",
|
|
293
|
+
"marks.import.paste.label": "AI response",
|
|
294
|
+
"marks.import.paste.help": "Paste the JSON returned by the model into the box below, then click Import.",
|
|
295
|
+
"marks.import.paste.aria": "JSON returned by the model",
|
|
296
|
+
"marks.import.paste.placeholder": "Supports json code fences or plain JSON, e.g. { \"tags\": [\"tag1\", \"tag2\"], \"note\": \"session note\" }",
|
|
297
|
+
"marks.import.paste.clear": "Clear input",
|
|
298
|
+
"marks.import.empty": "Paste the JSON returned by the model into the box above first.",
|
|
299
|
+
"marks.import.unknown": "unknown field",
|
|
300
|
+
"marks.import.discard": "Importing will overwrite your unsaved edits. Continue?",
|
|
301
|
+
"marks.import.failure": "Import failed: {message}",
|
|
302
|
+
"marks.import.warnings": "Imported, with adjustments:",
|
|
303
|
+
"marks.import.success": "Imported AI result from the clipboard",
|
|
304
|
+
"marks.tags.clear": "Clear tags",
|
|
305
|
+
"marks.note.clear": "Clear note",
|
|
306
|
+
"header.aria": "Session actions",
|
|
307
|
+
"menu.open": "Open",
|
|
308
|
+
"menu.archive": "Archive session",
|
|
309
|
+
"menu.unarchive": "Unarchive session",
|
|
310
|
+
"menu.delete": "Delete session…",
|
|
311
|
+
"menu.move": "Move to workspace…",
|
|
312
|
+
"menu.manage": "Open session manager…",
|
|
313
|
+
"footer.aria": "Session manager",
|
|
314
|
+
"footer.label": "Sessions",
|
|
315
|
+
"panel.title": "Session manager",
|
|
316
|
+
"panel.github.title": "Star or open an issue",
|
|
317
|
+
"panel.search": "Search title, ID, tags or notes",
|
|
318
|
+
"panel.workspace": "Workspace",
|
|
319
|
+
"panel.workspace.all": "All workspaces",
|
|
320
|
+
"panel.workspace.ungrouped": "Ungrouped",
|
|
321
|
+
"panel.workspace.loading": "Loading workspaces…",
|
|
322
|
+
"panel.workspace.error": "Could not load workspaces: {message}",
|
|
323
|
+
"panel.workspace.retry": "Retry",
|
|
324
|
+
"panel.sort": "Sort by",
|
|
325
|
+
"panel.sort.unavailable": "Creation times are unavailable in this DSH version; showing source order.",
|
|
326
|
+
"panel.sort.updated-desc": "Recently updated",
|
|
327
|
+
"panel.sort.updated-asc": "Least recently updated",
|
|
328
|
+
"panel.sort.created-desc": "Newest created",
|
|
329
|
+
"panel.sort.created-asc": "Oldest created",
|
|
330
|
+
"panel.count": "Showing {shown} / {total} sessions",
|
|
331
|
+
"panel.reset": "Reset filters",
|
|
332
|
+
"panel.empty.filtered": "No matching sessions. Adjust your search or filters.",
|
|
333
|
+
"bulk.title": "Bulk management",
|
|
334
|
+
"bulk.selectedFiltered": "Selected {selected} / {filtered}",
|
|
335
|
+
"bulk.selectAllFiltered": "Select all in filter",
|
|
336
|
+
"bulk.clear": "Clear selection",
|
|
337
|
+
"bulk.archive": "Archive",
|
|
338
|
+
"bulk.unarchive": "Unarchive",
|
|
339
|
+
"bulk.delete": "Delete session",
|
|
340
|
+
"bulk.move": "Move to workspace",
|
|
341
|
+
"bulk.moveBtn": "Move",
|
|
342
|
+
"bulk.presetMigrate": "Migrate preset",
|
|
343
|
+
"bulk.favorite": "Favorite",
|
|
344
|
+
"bulk.unfavorite": "Unfavorite",
|
|
345
|
+
"bulk.review": "Mark for review",
|
|
346
|
+
"bulk.unreview": "Clear review flag",
|
|
347
|
+
"bulk.tagsAdd": "Add tags",
|
|
348
|
+
"bulk.tagsRemove": "Clear tags",
|
|
349
|
+
"bulk.priority": "Set priority",
|
|
350
|
+
"bulk.tags.placeholder": "Comma-separated, e.g. bug, design",
|
|
351
|
+
"bulk.priority.aria": "Choose priority",
|
|
352
|
+
"bulk.workspace.aria": "Choose target workspace",
|
|
353
|
+
"bulk.preset.aria": "Choose target preset",
|
|
354
|
+
"bulk.dialog.title": "Bulk action: {action}",
|
|
355
|
+
"bulk.dialog.summary": "Apply \"{action}\" to {count} sessions.",
|
|
356
|
+
"bulk.dialog.warning.delete": "Deletion is permanent and cannot be undone.",
|
|
357
|
+
"bulk.confirm.archive": "Archive",
|
|
358
|
+
"bulk.confirm.unarchive": "Unarchive",
|
|
359
|
+
"bulk.confirm.favorite": "Favorite",
|
|
360
|
+
"bulk.confirm.unfavorite": "Unfavorite",
|
|
361
|
+
"bulk.confirm.review": "Mark review",
|
|
362
|
+
"bulk.confirm.unreview": "Clear review",
|
|
363
|
+
"bulk.confirm.tagsAdd": "Add tags",
|
|
364
|
+
"bulk.confirm.tagsRemove": "Clear tags",
|
|
365
|
+
"bulk.confirm.priority": "Set priority",
|
|
366
|
+
"bulk.confirm.move": "Move",
|
|
367
|
+
"bulk.confirm.presetMigrate": "Migrate",
|
|
368
|
+
"bulk.confirm.delete": "Delete",
|
|
369
|
+
"bulk.dialog.execute": "Execute",
|
|
370
|
+
"bulk.dialog.cancel": "Cancel",
|
|
371
|
+
"bulk.progress.title": "Running: {action}",
|
|
372
|
+
"bulk.progress.current": "Processing {done} / {total}",
|
|
373
|
+
"bulk.progress.cancel": "Cancel",
|
|
374
|
+
"bulk.progress.completed": "Finished {done} / {total}",
|
|
375
|
+
"bulk.result.title": "Bulk action finished: {action}",
|
|
376
|
+
"bulk.result.successGroup": "Success ({count})",
|
|
377
|
+
"bulk.result.failedGroup": "Failed ({count})",
|
|
378
|
+
"bulk.result.skippedGroup": "Skipped ({count})",
|
|
379
|
+
"bulk.result.retry": "Retry failed",
|
|
380
|
+
"bulk.result.close": "Close",
|
|
381
|
+
"bulk.error.tooMany": "Batch actions support at most 200 sessions; please narrow the filter.",
|
|
382
|
+
"bulk.checkbox.aria": "Select {title}",
|
|
383
|
+
"bulk.heading": "Bulk actions",
|
|
384
|
+
"panel.selectMode": "Batch process",
|
|
385
|
+
"panel.selectMode.exit": "Exit batch process",
|
|
386
|
+
"panel.selectMode.aria": "Enter bulk selection mode",
|
|
387
|
+
"panel.selectMode.active.aria": "Exit bulk selection mode",
|
|
388
|
+
"panel.filter.all": "All",
|
|
389
|
+
"panel.filter.active": "Active",
|
|
390
|
+
"panel.filter.archived": "Archived",
|
|
391
|
+
"panel.empty": "No sessions",
|
|
392
|
+
"panel.empty.archived": "No archived sessions",
|
|
393
|
+
"panel.archived": "Archived",
|
|
394
|
+
"panel.running": "Running",
|
|
395
|
+
"panel.current": "Current",
|
|
396
|
+
"panel.session.new": "New session",
|
|
397
|
+
"panel.close": "Close",
|
|
398
|
+
"row.open": "Open",
|
|
399
|
+
"row.archive": "Archive",
|
|
400
|
+
"row.unarchive": "Unarchive",
|
|
401
|
+
"row.move": "Move to workspace",
|
|
402
|
+
"row.delete": "Delete session",
|
|
403
|
+
"row.migrate": "Migrate preset",
|
|
404
|
+
"migrate.title": "Migrate Agent preset",
|
|
405
|
+
"migrate.current": "Current preset",
|
|
406
|
+
"migrate.target": "Target preset",
|
|
407
|
+
"migrate.noPresets": "No target presets are available",
|
|
408
|
+
"migrate.same": "Choose a target preset different from the current preset",
|
|
409
|
+
"migrate.confirm": "Migrate",
|
|
410
|
+
"migrate.completed": "Agent preset migrated to {preset}",
|
|
411
|
+
"confirm.delete.title": "Delete session",
|
|
412
|
+
"confirm.delete.desc": "Session \"{title}\" will be permanently deleted, including all its messages and files on disk. This cannot be undone.",
|
|
413
|
+
"confirm.delete.running": "This session is running; deleting will interrupt it immediately.",
|
|
414
|
+
"confirm.delete.confirm": "Delete",
|
|
415
|
+
"confirm.move.title": "Move session to workspace",
|
|
416
|
+
"confirm.move.desc": "Move session \"{title}\" to the target workspace.",
|
|
417
|
+
"confirm.move.running": "This session is running; moving will interrupt and close it; you can reopen it from the target workspace to continue.",
|
|
418
|
+
"confirm.move.empty": "No other workspaces to move to.",
|
|
419
|
+
"confirm.move.select": "Select target workspace",
|
|
420
|
+
"confirm.move.current": "Current workspace: {name}",
|
|
421
|
+
"confirm.move.confirm": "Move",
|
|
422
|
+
"confirm.cancel": "Cancel",
|
|
423
|
+
"busy.processing": "Working…",
|
|
424
|
+
"error.operation": "Operation failed: {message}",
|
|
425
|
+
"workspace.default": "Default workspace",
|
|
426
|
+
"workspace.sessions": "{count} sessions"
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const css = [
|
|
430
|
+
".sm-header{display:flex;align-items:center;gap:6px}",
|
|
431
|
+
".sm-headerBtn{box-sizing:border-box;min-height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;align-items:center;gap:4px;padding:3px 10px;font-size:12px;line-height:18px;display:inline-flex;white-space:nowrap}",
|
|
432
|
+
".sm-headerBtn:hover:not(:disabled),.sm-headerBtn:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
433
|
+
".sm-headerBtn:disabled{opacity:.5;cursor:not-allowed}",
|
|
434
|
+
".sm-headerBtn svg{flex:none}",
|
|
435
|
+
".sm-busy{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:28px;margin-left:4px}",
|
|
436
|
+
".sm-footer{display:flex}",
|
|
437
|
+
".sm-footerBtn{min-height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;border-radius:6px;align-items:center;gap:6px;padding:3px 8px;font-size:12px;line-height:18px;display:inline-flex}",
|
|
438
|
+
".sm-footerBtn:hover,.sm-footerBtn:focus-visible{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
439
|
+
".sm-footerBtn svg{flex:none}",
|
|
440
|
+
".sm-panel{display:flex;flex-direction:column;gap:10px;min-height:280px;max-height:min(70vh,640px);min-width:480px}",
|
|
441
|
+
".sm-filter{display:flex;gap:4px;flex-wrap:wrap}",
|
|
442
|
+
".sm-filterBtn{cursor:pointer;display:inline-flex;align-items:center;color:var(--dsw-alias-label-secondary);background:0 0;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;height:28px;padding:3px 10px;font-size:12px;line-height:18px;white-space:nowrap;box-sizing:border-box}",
|
|
443
|
+
".sm-filterBtn:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
444
|
+
".sm-filterBtn.sm-on{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-fill-l2);border-color:transparent}",
|
|
445
|
+
".sm-list{display:flex;flex-direction:column;gap:4px;min-height:0;overflow:auto;flex:1}",
|
|
446
|
+
".sm-row{box-sizing:border-box;display:flex;align-items:center;gap:10px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px}",
|
|
447
|
+
".sm-row:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
448
|
+
".sm-rowCurrent{border-color:var(--dsw-alias-accent-primary,var(--dsw-alias-label-secondary))}",
|
|
449
|
+
".sm-rowMain{min-width:0;display:flex;flex-direction:column;gap:2px;flex:1 1 280px}",
|
|
450
|
+
".sm-rowTitle{color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;display:flex;align-items:center;gap:6px;min-width:0}",
|
|
451
|
+
".sm-rowTitle>span:first-child{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}",
|
|
452
|
+
".sm-rowMeta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}",
|
|
453
|
+
".sm-rowActions{display:flex;gap:4px;flex:none;flex-wrap:wrap;justify-content:flex-end}",
|
|
454
|
+
".sm-badge{flex:none;border-radius:999px;padding:0 6px;font-size:10px;line-height:16px}",
|
|
455
|
+
".sm-badgeArchived{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-l2)}",
|
|
456
|
+
".sm-badgeRunning{color:var(--dsw-alias-state-warning-primary,#d97706);background:rgba(217,119,6,.12)}",
|
|
457
|
+
".sm-badgeCurrent{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-fill-l2)}",
|
|
458
|
+
".sm-danger{color:var(--dsw-alias-state-error-primary) !important;border-color:var(--dsw-alias-state-error-primary) !important}",
|
|
459
|
+
".sm-dangerText{color:var(--dsw-alias-state-error-primary) !important}",
|
|
460
|
+
".sm-warn{display:flex;align-items:flex-start;gap:8px;color:var(--dsw-alias-label-secondary);background:rgba(217,119,6,.08);border:1px solid rgba(217,119,6,.25);border-radius:8px;padding:8px 10px;margin-top:4px}",
|
|
461
|
+
".sm-warn svg{flex:none;margin-top:1px}",
|
|
462
|
+
".sm-error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;min-height:0;margin-top:4px}",
|
|
463
|
+
".sm-foot{display:flex;justify-content:flex-end;align-items:center;gap:8px;width:100%}",
|
|
464
|
+
".sm-footError{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;margin-right:auto}",
|
|
465
|
+
".sm-empty{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px;text-align:center;padding:24px 0}",
|
|
466
|
+
".sm-moveSection{display:flex;flex-direction:column;gap:8px}",
|
|
467
|
+
".sm-moveLabel{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}",
|
|
468
|
+
".sm-moveList{display:flex;flex-direction:column;gap:4px;max-height:200px;overflow:auto}",
|
|
469
|
+
".sm-moveItem{box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;transition:all .15s}",
|
|
470
|
+
".sm-moveItem:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
471
|
+
".sm-moveItemSelected{border-color:var(--dsw-alias-accent-primary);background:var(--dsw-alias-accent-primary-bg)}",
|
|
472
|
+
".sm-moveItemName{color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px}",
|
|
473
|
+
".sm-moveItemMeta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}.sm-panelModal{width:760px;max-width:92vw}.sm-panelModal .sm-rowActions{flex-wrap:wrap;justify-content:flex-end}.sm-headerMenu{position:absolute;right:0;top:100%;z-index:50;display:flex;flex-direction:column;min-width:160px;padding:4px;background:var(--dsw-alias-surface-l1);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,.12)}.sm-headerMenuItem{display:flex;align-items:center;width:100%;text-align:left;background:0 0;border:0;border-radius:6px;color:var(--dsw-alias-label-primary);cursor:pointer;padding:6px 10px;font-size:13px;line-height:18px}.sm-headerMenuItem:hover{background:var(--dsw-alias-interactive-bg-hover)}.sm-headerMenuItemDanger{color:var(--dsw-alias-state-error-primary)}.sm-headerBtn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:32px;height:auto;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-l2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;padding:4px 12px;font-size:13px;line-height:18px;white-space:nowrap;transition:background .15s,color .15s,border-color .15s}.sm-headerBtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l1)}.sm-headerBtn:focus-visible{outline:2px solid var(--dsw-alias-accent-primary);outline-offset:1px}.sm-headerBtn svg{width:16px;height:16px}.sm-migratePanel{display:flex;flex-direction:column;gap:14px;min-width:480px}.sm-migrateRow{display:flex;flex-direction:column;gap:8px}.sm-migrateRow>label{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:16px;font-weight:500}.sm-migrateChips{display:flex;flex-wrap:wrap;gap:6px}.sm-migrateChip{padding:6px 12px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:transparent;cursor:pointer;font-size:13px;line-height:18px;color:var(--dsw-alias-label-primary);white-space:nowrap}.sm-migrateChip:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.sm-migrateChip:disabled{opacity:.5;cursor:not-allowed}.sm-migrateChipOn{background:var(--dsw-alias-accent-primary);color:#fff;border-color:var(--dsw-alias-accent-primary)}.sm-migrateEmpty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.sm-migratePreview{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-fill-l2);color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px}.sm-migrateWarn{padding:10px 12px;border:1px solid rgba(217,119,6,.25);border-radius:8px;background:rgba(217,119,6,.08);color:var(--dsw-alias-label-secondary);font-size:13px;line-height:18px}.sm-migrateList{max-height:200px;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:8px}.sm-headerBtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l1)}.sm-headerBtnDanger{background:var(--dsw-alias-state-error-tertiary);color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);font-weight:500}.sm-headerBtnDanger:hover:not(:disabled){background:#d92d20!important;color:#fff!important;border-color:#d92d20!important}.sm-header{display:inline-flex;align-items:center;gap:6px}.sm-annotationToggleOpen{position:relative;z-index:10004}.sm-headerBtnActive{background:var(--dsw-alias-fill-l3);color:var(--dsw-alias-label-primary)}",
|
|
474
|
+
".sm-panelDialog{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);width:600px;max-width:calc(100vw - 32px);max-height:560px;height:auto;box-sizing:border-box;display:flex;flex-direction:column;gap:0;background:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.25);overflow:hidden;z-index:9999;opacity:1}.sm-panelDialog .sm-nativeDialogHeader{display:flex;align-items:center;gap:8px;padding:10px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#eee);flex:none}.sm-panelDialog .sm-nativeDialogTitle{margin:0;font-size:14px;line-height:20px;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:center;flex:1 1 auto}.sm-panelDialog .sm-nativeDialogHeaderActions{display:flex;align-items:center;gap:6px;flex:none}.sm-panelDialog .sm-nativeDialogTitle{margin:0;font-size:14px;line-height:20px;font-weight:600;text-align:center;flex:1 1 auto}.sm-nativeDialogFooter,.sm-bulkDialog .sm-nativeDialogFooter,.sm-confirmDialog .sm-nativeDialogFooter,.sm-migrateDialog .sm-nativeDialogFooter{display:flex;padding:10px 16px;border-top:1px solid var(--dsw-alias-border-l2,#eee);flex:none;flex-wrap:wrap;justify-content:space-between;gap:6px;margin-top:0}.sm-panelDialog .sm-panel{display:flex;flex-direction:column;gap:6px;min-height:0;flex:1 1 auto;overflow:auto;padding:8px 12px}.sm-panelDialog .sm-list{display:flex;flex-direction:column;gap:2px;flex:1 1 auto;min-height:0;overflow:auto}.sm-panelDialog .sm-row{display:flex;align-items:center;gap:8px;padding:6px 10px;min-height:36px;border:1px solid transparent;border-radius:8px;background:transparent;flex-shrink:0}.sm-panelDialog .sm-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.sm-panelDialog .sm-rowCurrent{background:var(--dsw-alias-fill-l2);border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-panelDialog .sm-rowMain{display:flex;align-items:stretch!important;gap:8px;min-width:0;flex:1 1 auto;overflow:visible;text-align:left!important}.sm-panelDialog .sm-rowTitle{min-width:0;flex:1 1 0%;width:100%;align-self:stretch;display:flex;align-items:center;justify-content:flex-start;gap:6px;font-size:12px;line-height:16px;overflow:hidden;text-align:left!important;direction:ltr!important}.sm-confirmDialog{z-index:10000}.sm-confirmDialog.sm-nativeDialogLayer{position:fixed;left:calc(50vw + 184px);top:calc(50vh - 90px);width:360px;max-width:calc(100vw - 32px);max-height:none;display:block;align-items:initial;justify-content:initial;box-sizing:border-box;background:transparent;border:0;box-shadow:none;padding:0;inset:calc(50vh - 90px) auto auto calc(50vw + 308px)}.sm-confirmDialog.sm-nativeDialogLayer .sm-nativeDialogBackdrop{display:none}.sm-confirmDialog.sm-nativeDialogLayer .sm-nativeDialog{position:relative;left:auto;top:auto;transform:none;width:auto;box-sizing:border-box;background:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.25);padding:0;overflow:hidden;animation:sm-confirmPop .14s ease-out}.sm-confirmDialog .sm-nativeDialogHeader{padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#eee)}.sm-confirmDialog .sm-nativeDialogTitle{margin:0;font-size:14px;line-height:20px;font-weight:600;text-align:center;width:100%}.sm-confirmDialog .sm-nativeDialogBody{padding:10px 16px;display:flex;flex-direction:column;gap:8px;min-height:0}.sm-confirmDialog .sm-nativeDialogDescription{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap}.sm-confirmDialog .sm-warn{padding:6px 10px;font-size:12px;line-height:16px;margin-top:0}.sm-confirmDialog .sm-nativeDialogFooter{padding:10px 16px;border-top:1px solid var(--dsw-alias-border-l2,#eee);flex-wrap:wrap;justify-content:space-between;gap:6px;margin-top:0}.sm-confirmDialog .sm-nativeDialogButton{min-height:28px;padding:3px 14px;font-size:12px;line-height:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;background:transparent;color:var(--dsw-alias-label-primary);text-align:center}.sm-confirmDialog .sm-nativeDialogCancel:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.sm-confirmDialog .sm-nativeDialogConfirm{background:var(--dsw-alias-accent-primary,#356ae6);color:#fff;border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-confirmDialog .sm-nativeDialogConfirm:hover:not(:disabled){filter:brightness(.94)}@keyframes sm-confirmPop{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.sm-confirmDialog.sm-nativeDialog{padding:0;width:auto;max-width:none}.sm-panelDialog .sm-rowTitle>span:first-child{display:block;min-width:0;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;flex:1 1 auto;width:auto;text-align:left!important;direction:ltr!important;unicode-bidi:plaintext}.sm-panelDialog .sm-rowMeta{display:none}.sm-panelDialog .sm-rowActions{display:flex;gap:2px;flex:none;justify-content:flex-end}.sm-panelDialog .sm-rowActions .sm-rowBtn{min-height:22px;padding:0 7px;font-size:11px;line-height:18px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap}.sm-rowBtn.sm-markIcon{padding:0 6px;font-size:13px;line-height:18px}.sm-panelDialog .sm-rowActions .sm-rowBtn:hover{background:var(--dsw-alias-fill-l2);color:var(--dsw-alias-label-primary)}.sm-panelDialog .sm-rowActions .sm-rowBtnDanger{color:var(--dsw-alias-state-error-primary)}.sm-panelDialog .sm-filter{flex:none;padding:6px 12px 4px;gap:4px;display:flex;flex-wrap:wrap}.sm-panelDialog .sm-filterBtn{min-height:24px;padding:2px 10px;font-size:11px;line-height:16px;border-radius:4px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}.sm-panelDialog .sm-filterBtn.sm-on{background:var(--dsw-alias-fill-l2);color:var(--dsw-alias-label-primary)}.sm-panelDialog .sm-badge{font-size:10px;line-height:14px;padding:0 5px;border-radius:6px;flex:none}.sm-panelDialog .sm-error{padding:6px 10px;margin:0;font-size:12px;line-height:16px}.sm-panelDialog .sm-nativeDialogButton{min-height:28px;padding:3px 12px;font-size:12px;line-height:18px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);cursor:pointer}.sm-panelDialog .sm-nativeDialogCancel{background:transparent}.sm-panelDialog .sm-nativeDialogGhost{background:transparent;border-color:var(--dsw-alias-border-l2)}.sm-panelDialog .sm-nativeDialogConfirm{background:var(--dsw-alias-accent-primary,#356ae6);color:#fff;border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-nativeDialogDanger{background:var(--dsw-alias-state-error-primary,#d92d20);color:#fff;border-color:var(--dsw-alias-state-error-primary,#d92d20)}.sm-nativeDialogDanger:hover:not(:disabled){filter:brightness(.92)}.sm-confirmDialog .sm-nativeDialogDanger,.sm-migrateDialog .sm-nativeDialogDanger,.sm-bulkDialog .sm-nativeDialogDanger{background:var(--dsw-alias-state-error-primary,#d92d20);color:#fff;border-color:var(--dsw-alias-state-error-primary,#d92d20)}.sm-confirmDialog .sm-nativeDialogDanger:hover:not(:disabled),.sm-migrateDialog .sm-nativeDialogDanger:hover:not(:disabled),.sm-bulkDialog .sm-nativeDialogDanger:hover:not(:disabled){filter:brightness(.92)}.sm-panelDialog .sm-empty{padding:18px 0;font-size:12px;color:var(--dsw-alias-label-tertiary);text-align:center}.sm-panelDialog .sm-footError{font-size:11px;color:var(--dsw-alias-state-error-primary);margin-right:auto;line-height:20px}.sm-panelDialog .sm-moveLabel{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}.sm-migrateDialog .sm-nativeDialogFooter{flex-wrap:wrap;justify-content:space-between;gap:6px;margin-top:0}.sm-migrateDialog .sm-migrateChip{cursor:pointer;pointer-events:auto;position:relative;z-index:1}.sm-migrateDialog .sm-migrateChip[disabled]{opacity:.5;cursor:not-allowed}.sm-migrateDialog.sm-nativeDialogLayer{position:fixed;left:50vw;top:auto;bottom:48px;transform:translateX(-50%);width:min(560px,calc(100vw - 32px));max-height:none;display:block;align-items:initial;justify-content:initial;box-sizing:border-box;background:transparent;border:0;box-shadow:none;padding:0;inset:auto;z-index:10001}.sm-migrateDialog.sm-nativeDialogLayer .sm-nativeDialogBackdrop{display:none}.sm-migrateDialog.sm-nativeDialogLayer .sm-nativeDialog{position:relative;left:auto;top:auto;transform:none;width:360px;max-width:calc(100vw - 32px);max-height:min(70vh,520px);box-sizing:border-box;background-color:#ffffff;background-color:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.45);overflow:hidden;padding:0;display:flex;flex-direction:column;gap:0;opacity:1}.sm-migrateDialog{position:fixed;left:50vw;top:auto;bottom:48px;transform:translateX(-50%);width:min(560px,calc(100vw - 32px));max-height:min(70vh,520px);box-sizing:border-box;display:flex;flex-direction:column;gap:0;background-color:#ffffff;background-color:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.45);overflow:hidden;opacity:1;z-index:10001;backdrop-filter:none;-webkit-backdrop-filter:none}.sm-migrateDialog .sm-nativeDialogHeader{padding:12px 18px;border-bottom:1px solid #eee;border-bottom:1px solid var(--dsw-alias-border-l2,#eee);flex:none;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-nativeDialogTitle{margin:0;font-size:14px;line-height:20px;font-weight:600;color:#111;color:var(--dsw-alias-label-primary,#111);text-align:center;width:100%}.sm-migrateDialog .sm-migratePanel{padding:12px 18px;display:flex;flex-direction:column;gap:14px;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-nativeDialogBody{padding:0;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-nativeDialogFooter{padding:10px 16px;border-top:1px solid var(--dsw-alias-border-l2,#eee);flex:none;flex-wrap:wrap;justify-content:space-between;gap:6px;margin-top:0;gap:8px;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-migrateRow>label{color:#444;color:var(--dsw-alias-label-secondary,#444)}.sm-migrateDialog .sm-migrateChip{padding:6px 12px;border-radius:999px;border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);background-color:#f5f5f5;background-color:var(--dsw-alias-fill-l2,#f5f5f5);color:#111;color:var(--dsw-alias-label-primary,#111);font-size:13px;line-height:18px;cursor:pointer;pointer-events:auto;position:relative;z-index:1;white-space:nowrap}.sm-migrateDialog .sm-migrateChip:hover:not(:disabled){background-color:#e8e8e8;background-color:var(--dsw-alias-interactive-bg-hover,#e8e8e8)}.sm-migrateDialog .sm-migrateChipOn{background-color:#356ae6;background-color:var(--dsw-alias-accent-primary,#356ae6);color:#fff;border-color:#356ae6;border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-migrateDialog .sm-migrateChip.sm-migrateChipOn:hover:not(:disabled){background-color:#4576f0;background-color:var(--dsw-alias-accent-primary-hover,#4576f0);color:#fff;border-color:#4576f0;border-color:var(--dsw-alias-accent-primary-hover,#4576f0);filter:none}.sm-migrateDialog .sm-migrateChipOn:hover:not(:disabled){filter:brightness(.92)}.sm-migrateDialog .sm-migrateChip[disabled]{opacity:.5;cursor:not-allowed}.sm-migrateDialog .sm-migrateEmpty{color:#888;color:var(--dsw-alias-label-tertiary,#888)}.sm-migrateDialog .sm-migratePreview{padding:10px 12px;border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;background-color:#f5f5f5;background-color:var(--dsw-alias-fill-l2,#f5f5f5);color:#111;color:var(--dsw-alias-label-primary,#111);font-size:13px;line-height:18px}.sm-migrateDialog .sm-migrateWarn{padding:10px 12px;border:1px solid rgba(217,119,6,.25);border-radius:8px;background-color:rgba(217,119,6,.08);color:#444;color:var(--dsw-alias-label-secondary,#444);font-size:13px;line-height:18px}.sm-migrateDialog .sm-nativeDialogButton{min-height:28px;padding:3px 14px;font-size:12px;line-height:20px;border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;cursor:pointer;background-color:#fff;background-color:var(--dsw-alias-fill-l2,#fff);color:#111;color:var(--dsw-alias-label-primary,#111);text-align:center}.sm-migrateDialog .sm-nativeDialogButton:hover:not(:disabled){background-color:#e8e8e8;background-color:var(--dsw-alias-interactive-bg-hover,#e8e8e8)}.sm-migrateDialog .sm-nativeDialogConfirm{background-color:#356ae6;background-color:var(--dsw-alias-accent-primary,#356ae6);color:#fff;border-color:#356ae6;border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-migrateDialog .sm-nativeDialogConfirm:hover:not(:disabled){filter:brightness(.94)}",
|
|
475
|
+
".sm-panelDialog .sm-nativeDialogHeader{display:flex;align-items:center;gap:8px}.sm-panelHeaderClose{min-height:26px;padding:2px 9px;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary,#555);cursor:pointer;font-size:12px;line-height:18px;flex:none}.sm-panelHeaderGithub{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:6px;color:var(--dsw-alias-label-secondary,#555);text-decoration:none;flex:none;transition:background .15s,color .15s;cursor:pointer}.sm-panelHeaderGithub:hover{background:var(--dsw-alias-interactive-bg-hover,#eee);color:var(--dsw-alias-label-primary,#111)}.sm-panelHeaderGithub svg{display:block;width:16px;height:16px;fill:currentColor}.sm-panelHeaderClose:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#eee);color:var(--dsw-alias-label-primary,#111)}.sm-panelHeaderClose:disabled{opacity:.5;cursor:not-allowed}.sm-panelHeaderSelect{min-height:26px;padding:2px 9px;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary,#555);cursor:pointer;font-size:12px;line-height:18px;display:inline-flex;align-items:center;gap:4px}.sm-panelHeaderSelect:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#eee);color:var(--dsw-alias-label-primary,#111)}.sm-panelHeaderSelect:disabled{opacity:.5;cursor:not-allowed}.sm-panelHeaderSelect.sm-on{background:var(--dsw-alias-accent-primary-bg,rgba(53,106,230,.12));border-color:var(--dsw-alias-accent-primary,#356ae6);color:var(--dsw-alias-accent-primary,#356ae6)}.sm-migrateCurrent{padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;background:var(--dsw-alias-fill-l2,#f5f5f5);font-size:13px;line-height:18px;color:var(--dsw-alias-label-primary,#111)}.sm-migrateConfirmLayer.sm-confirmDialog.sm-nativeDialogLayer{width:360px;inset:calc(50vh - 90px) auto auto calc(50vw + 308px)}.sm-migrateConfirmLayer.sm-confirmDialog.sm-nativeDialogLayer .sm-migrateDialog{position:relative;left:auto;top:auto;bottom:auto;transform:none;width:100%;max-width:none;max-height:min(70vh,520px);z-index:auto}.sm-migrateConfirmLayer .sm-migratePanel{min-width:0}.sm-migrateConfirmLayer .sm-nativeDialogButton.sm-nativeDialogConfirm:hover:not(:disabled){background-color:#4576f0;background-color:var(--dsw-alias-accent-primary-hover,#4576f0);color:#fff;border-color:#4576f0;border-color:var(--dsw-alias-accent-primary-hover,#4576f0);filter:none}",
|
|
476
|
+
|
|
477
|
+
// Theme colors are scoped to the plugin dialogs. DSH exposes its
|
|
478
|
+
// actual theme through `body[data-ds-dark-theme]` and color-scheme;
|
|
479
|
+
// keeping these values local avoids changing DSH's global variables.
|
|
480
|
+
"[data-sm-theme=light] .sm-panelDialog,[data-sm-theme=light] .sm-confirmDialog.sm-nativeDialogLayer .sm-nativeDialog,[data-sm-theme=light] .sm-migrateDialog.sm-nativeDialogLayer .sm-nativeDialog{--dsw-alias-surface-l1:#fff;--dsw-alias-label-primary:#1f1f1f;--dsw-alias-label-secondary:#555;--dsw-alias-label-tertiary:#888;--dsw-alias-border-l1:#d4d4d8;--dsw-alias-border-l2:#e5e5e5;--dsw-alias-fill-l2:#f5f5f5;--dsw-alias-interactive-bg-hover:#eee;--dsw-alias-state-error-primary:#d92d20;--dsw-alias-state-warning-primary:#d97706;--dsw-alias-accent-primary:#356ae6;--dsw-alias-accent-primary-bg:rgba(53,106,230,.12);background:#fff!important;background-color:#fff!important;color:#1f1f1f!important;border-color:#e5e5e5!important}",
|
|
481
|
+
"[data-sm-theme=dark] .sm-panelDialog,[data-sm-theme=dark] .sm-confirmDialog.sm-nativeDialogLayer .sm-nativeDialog,[data-sm-theme=dark] .sm-migrateDialog.sm-nativeDialogLayer .sm-nativeDialog,[data-sm-theme=dark] .sm-bulkDialog.sm-nativeDialogLayer .sm-nativeDialog{--dsw-alias-surface-l1:#1f1f23;--dsw-alias-label-primary:#ececec;--dsw-alias-label-secondary:#b3b3b3;--dsw-alias-label-tertiary:#999;--dsw-alias-border-l1:#555;--dsw-alias-border-l2:#3a3a40;--dsw-alias-fill-l1:#1a1a1e;--dsw-alias-fill-l2:#2a2a30;--dsw-alias-interactive-bg-hover:rgba(255,255,255,.06);--dsw-alias-state-error-primary:#f47171;--dsw-alias-state-warning-primary:#f0a040;--dsw-alias-accent-primary:#5d8aff;--dsw-alias-accent-primary-bg:rgba(93,138,255,.18);background:#1f1f23!important;background-color:#1f1f23!important;color:#ececec!important;border-color:#3a3a40!important}","[data-sm-theme=dark] .sm-header{--dsw-alias-surface-l1:#1f1f23;--dsw-alias-label-primary:#ececec;--dsw-alias-label-secondary:#b3b3b3;--dsw-alias-label-tertiary:#999;--dsw-alias-border-l1:#555;--dsw-alias-border-l2:#3a3a40;--dsw-alias-fill-l1:#1a1a1e;--dsw-alias-fill-l2:#2a2a30;--dsw-alias-interactive-bg-hover:rgba(255,255,255,.06);--dsw-alias-accent-primary:#5d8aff;--dsw-alias-accent-primary-bg:rgba(93,138,255,.18)}",
|
|
482
|
+
"[data-sm-theme=light] .sm-panelDialog .sm-nativeDialogHeader,[data-sm-theme=light] .sm-panelDialog .sm-nativeDialogFooter,[data-sm-theme=light] .sm-confirmDialog .sm-nativeDialogHeader,[data-sm-theme=light] .sm-confirmDialog .sm-nativeDialogFooter,[data-sm-theme=light] .sm-migrateDialog .sm-nativeDialogHeader,[data-sm-theme=light] .sm-migrateDialog .sm-nativeDialogBody,[data-sm-theme=light] .sm-migrateDialog .sm-nativeDialogFooter{background:#fff!important;background-color:#fff!important;border-color:#e5e5e5!important}",
|
|
483
|
+
"[data-sm-theme=dark] .sm-panelDialog .sm-nativeDialogHeader,[data-sm-theme=dark] .sm-panelDialog .sm-nativeDialogFooter,[data-sm-theme=dark] .sm-confirmDialog .sm-nativeDialogHeader,[data-sm-theme=dark] .sm-confirmDialog .sm-nativeDialogFooter,[data-sm-theme=dark] .sm-migrateDialog .sm-nativeDialogHeader,[data-sm-theme=dark] .sm-migrateDialog .sm-nativeDialogBody,[data-sm-theme=dark] .sm-migrateDialog .sm-nativeDialogFooter,[data-sm-theme=dark] .sm-bulkDialog .sm-nativeDialogHeader,[data-sm-theme=dark] .sm-bulkDialog .sm-nativeDialogBody,[data-sm-theme=dark] .sm-bulkDialog .sm-nativeDialogFooter{background:#1f1f23!important;background-color:#1f1f23!important;border-color:#3a3a40!important}[data-sm-theme=dark] .sm-bulkDialog .sm-bulkSessionList,[data-sm-theme=dark] .sm-bulkDialog .sm-bulkResultList,[data-sm-theme=dark] .sm-bulkDialog .sm-bulkProgressBar{background:#1a1a1e!important;background-color:#1a1a1e!important;border-color:#3a3a40!important;color:var(--dsw-alias-label-primary,#ececec)}[data-sm-theme=dark] .sm-bulkDialog .sm-bulkSessionItem,[data-sm-theme=dark] .sm-bulkDialog .sm-bulkResultItemTitle,[data-sm-theme=dark] .sm-bulkDialog .sm-bulkResultItem{color:var(--dsw-alias-label-primary,#ececec)}",
|
|
484
|
+
".sm-panelDialog{max-height:min(640px,calc(100vh - 32px))}.sm-panelDialog .sm-panel{min-width:0;overflow:hidden}",
|
|
485
|
+
".sm-panelTools{display:flex;flex-direction:column;gap:8px;flex:none;padding:2px 0 6px}",
|
|
486
|
+
".sm-queryInput,.sm-viewSelect{box-sizing:border-box;width:100%;min-width:0;height:32px;padding:5px 9px;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:7px;background:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);font:inherit;font-size:12px;line-height:20px}",
|
|
487
|
+
".sm-queryInput::placeholder{color:var(--dsw-alias-label-tertiary,#888)}.sm-queryInput:focus-visible,.sm-viewSelect:focus-visible,.sm-resetFilters:focus-visible{outline:2px solid var(--dsw-alias-accent-primary,#356ae6);outline-offset:2px}.sm-viewSelect:disabled{opacity:.6;cursor:wait}",
|
|
488
|
+
".sm-panelSelects{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.sm-panelSelects .sm-viewField>span{display:none}.sm-panelSelects .sm-viewField .sm-viewSelect{font-size:11px;padding:4px 7px;height:28px;line-height:18px}.sm-viewField{display:flex;flex-direction:column;gap:3px;min-width:0;color:var(--dsw-alias-label-secondary,#666);font-size:11px;line-height:16px}",
|
|
489
|
+
".sm-filterBar{display:flex;align-items:center;justify-content:space-between;gap:6px;flex-wrap:wrap}.sm-resultSummary{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:11px;color:var(--dsw-alias-label-tertiary,#888)}.sm-resetFilters{padding:2px 0;border:0;background:none;color:var(--dsw-alias-accent-primary,#356ae6);font:inherit;cursor:pointer}.sm-resetFilters:hover{text-decoration:underline}.sm-workspaceNotice{display:flex;gap:8px;align-items:center;font-size:12px;color:var(--dsw-alias-label-secondary,#666)}",
|
|
490
|
+
"@media(max-width:540px){.sm-panelSelects{grid-template-columns:minmax(0,1fr)}.sm-panelDialog .sm-row{height:auto;min-height:36px;flex-shrink:0;flex-wrap:wrap;padding:6px 8px}.sm-panelDialog .sm-rowMain{flex-basis:100%}.sm-panelDialog .sm-rowActions{width:100%;min-width:0;flex:1 1 100%;justify-content:flex-start}}",
|
|
491
|
+
".sm-panelDialog:not(.sm-annotationSurface){width:680px}.sm-panelDialog .sm-row{flex-shrink:0;padding:5px 8px}.sm-panelDialog .sm-rowMain{flex:1 1 200px;flex-direction:column;gap:4px}.sm-panelDialog .sm-rowActions{max-width:100%}.sm-rowSecondary{display:flex;flex-wrap:nowrap;align-items:center;gap:6px;min-width:0;line-height:18px}.sm-rowBadges{display:flex;align-items:center;gap:5px;min-width:0;flex:1 1 auto;flex-wrap:wrap;font-size:11px;line-height:18px}.sm-rowMeta{color:var(--dsw-alias-label-tertiary,#888);font-size:11px;line-height:18px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;flex:none}.sm-rowAnnotations{display:flex;align-items:center;gap:5px;min-width:0;font-size:10px;line-height:16px}",
|
|
492
|
+
".sm-markIcon{min-width:25px;padding-left:5px;padding-right:5px;font-size:15px}.sm-markIcon.sm-markOn{color:var(--dsw-alias-accent-primary,#356ae6)!important;background:var(--dsw-alias-fill-l2,#eee)}.sm-priorityBadge{padding:0 5px;border-radius:4px;background:var(--dsw-alias-fill-l2,#eee);color:var(--dsw-alias-label-secondary,#666)}.sm-priority-1{color:#c2410c;background:rgba(234,88,12,.12)}.sm-priority-2{color:#a16207;background:rgba(202,138,4,.12)}",
|
|
493
|
+
"[data-sm-theme=dark] .sm-priority-1{color:#fdba74}[data-sm-theme=dark] .sm-priority-2{color:#fde68a}",
|
|
494
|
+
".sm-tagChip,.sm-noteBadge{max-width:88px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border:0;border-radius:4px;background:var(--dsw-alias-fill-l2,#eee);color:var(--dsw-alias-label-secondary,#666);font:inherit;line-height:16px;padding:0 5px;cursor:pointer}.sm-noteBadge{background:transparent}.sm-tagChip:hover,.sm-noteBadge:hover{color:var(--dsw-alias-accent-primary,#356ae6)}",
|
|
495
|
+
".sm-markFilters{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.sm-markFiltersGroup{display:flex;flex-wrap:wrap;align-items:center;gap:6px}.sm-markFiltersSpacer{flex:1 1 auto;min-width:12px}.sm-markFilterToggle{height:28px;padding:3px 10px;white-space:nowrap}.sm-markFilterToggle:disabled{opacity:.5;cursor:not-allowed}.sm-bulkSelectBtn{height:28px;padding:3px 10px;white-space:nowrap}.sm-bulkSelectBtn:disabled{opacity:.5;cursor:not-allowed}.sm-header{flex-wrap:wrap}",
|
|
496
|
+
".sm-annotationDialog.sm-nativeDialogLayer{position:fixed;display:block;pointer-events:none;z-index:10002;background:transparent}.sm-annotationDialog .sm-nativeDialogBackdrop{display:none}.sm-annotationDialog.sm-annotationDialog-anchored.sm-nativeDialogLayer{inset:auto;background:transparent}.sm-annotationDialog.sm-confirmDialog.sm-nativeDialogLayer:not(.sm-annotationDialog-anchored){inset:50% auto auto calc(50vw + 308px);transform:translateY(-50%);max-height:calc(100vh - 32px)}.sm-panelDialog.sm-annotationSurface{position:absolute;width:360px;max-height:min(680px,calc(100vh - 32px));pointer-events:auto;z-index:10003;box-shadow:0 16px 48px rgba(0,0,0,.25)}.sm-panelDialog.sm-annotationSurface.sm-annotationSurface-anchored{position:relative;transform:none;left:auto;top:auto;margin:0}.sm-annotationCloseHint{margin:0 16px 8px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}",
|
|
497
|
+
".sm-annotationBody{padding:14px 16px;display:flex;flex-direction:column;gap:12px;min-height:0;overflow:auto;flex:1 1 auto}.sm-annotationTarget{font-size:12px;line-height:18px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary,#666)}.sm-annotationChecks{display:grid;grid-template-columns:auto 1fr;column-gap:14px;row-gap:4px;align-items:center;font-size:13px}.sm-annotationChecks>label{display:flex;gap:6px;align-items:center;cursor:pointer}.sm-annotationChecks .sm-annotationPriority{display:inline-flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:inherit}.sm-annotationChecks .sm-prioritySelectInline{height:24px;padding:2px 6px;font-size:12px;line-height:18px;min-width:104px;width:auto;border-radius:6px;color:var(--dsw-alias-label-primary,#111)}.sm-annotationChecks>small{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#888);justify-self:start}.sm-annotationHint,.sm-annotationBody small{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#888)}",
|
|
498
|
+
".sm-noteInput{box-sizing:border-box;width:100%;min-height:60px;resize:vertical;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:7px;padding:8px;background:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);font:inherit;font-size:13px;line-height:19px}.sm-noteInput:focus-visible{outline:2px solid var(--dsw-alias-accent-primary,#356ae6);outline-offset:2px}.sm-markConflict{display:flex;flex-direction:column;gap:5px;font-size:12px;padding:8px;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:6px}.sm-fieldWithClear .sm-fieldRow{position:relative;display:flex;align-items:flex-start;gap:4px}.sm-fieldRow .sm-queryInput{flex:1 1 auto;min-width:0}.sm-fieldRowNote{align-items:stretch}.sm-fieldClear{position:absolute;top:5px;right:6px;width:22px;height:22px;border:0;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;border-radius:4px;font-size:14px;line-height:18px;display:inline-flex;align-items:center;justify-content:center;padding:0}.sm-fieldClear:hover,.sm-fieldClear:focus-visible{color:var(--dsw-alias-accent-primary);background:var(--dsw-alias-interactive-bg-hover);outline:none}.sm-fieldClearNote{top:6px}.sm-importGroup{display:flex;flex-direction:column;gap:8px;padding:10px 16px;border-top:1px solid var(--dsw-alias-border-l2,#eee);background:var(--dsw-alias-fill-l1,#fafbfc)}.sm-importGroupButtons{display:flex;justify-content:space-between;align-items:center;gap:8px}.sm-importBtn{cursor:pointer;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-surface-l1,#fff);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;padding:5px 12px;font-size:12px;line-height:18px}.sm-importBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.sm-importBtn:disabled{opacity:.5;cursor:not-allowed}.sm-importStatus{margin:0 16px;padding:8px 10px;font-size:12px;line-height:18px;background:var(--dsw-alias-accent-primary-bg,rgba(53,106,230,.08));color:var(--dsw-alias-label-secondary);border-radius:6px;white-space:pre-wrap;word-break:break-word}",
|
|
499
|
+
".sm-bulkBar{box-sizing:border-box;display:grid;grid-template-columns:repeat(6,minmax(0,1fr));align-items:stretch;gap:6px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:10px;background:var(--dsw-alias-fill-l1,#fafbfc)}.sm-bulkBar .sm-bulkSummary{grid-column:1/-1;color:var(--dsw-alias-label-primary,#111);font-size:12px;line-height:18px;font-weight:500;margin:0 0 2px}",
|
|
500
|
+
".sm-bulkSummary{color:var(--dsw-alias-label-primary,#111);font-size:12px;line-height:18px;font-weight:500}",
|
|
501
|
+
"",
|
|
502
|
+
".sm-bulkBar .sm-bulkBtn{cursor:pointer;color:var(--dsw-alias-label-secondary,#666);background:var(--dsw-alias-surface-l1,#fff);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;padding:4px 10px;font-size:12px;line-height:18px;display:inline-flex;align-items:center;justify-content:center;gap:4px;white-space:nowrap;text-align:center}",
|
|
503
|
+
".sm-bulkBar .sm-bulkBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary,#111)}",
|
|
504
|
+
".sm-bulkBar .sm-bulkBtn:disabled{opacity:.5;cursor:not-allowed}",
|
|
505
|
+
".sm-bulkBar .sm-bulkBtnDanger{color:var(--dsw-alias-state-error-primary);border-color:rgba(217,80,80,.4)}",
|
|
506
|
+
".sm-bulkBar .sm-bulkBtnDanger:hover:not(:disabled){color:#fff;background:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}",
|
|
507
|
+
"",
|
|
508
|
+
".sm-bulkRowCheckbox{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;flex:none}",
|
|
509
|
+
".sm-bulkRowCheckbox input{width:14px;height:14px;cursor:pointer;margin:0}",
|
|
510
|
+
".sm-bulkRowCheckbox input:disabled{cursor:not-allowed}",
|
|
511
|
+
".sm-rowBulkSelected{border-color:var(--dsw-alias-accent-primary,#356ae6);background:var(--dsw-alias-accent-primary-bg,rgba(53,106,230,.06))}",
|
|
512
|
+
".sm-bulkDialog.sm-nativeDialogLayer{position:fixed;left:calc(50vw + 184px);top:calc(50vh - 90px);width:360px;max-width:calc(100vw - 32px);max-height:none;display:block;align-items:initial;justify-content:initial;box-sizing:border-box;background:transparent;border:0;box-shadow:none;padding:0;inset:calc(50vh - 90px) auto auto calc(50vw + 308px);z-index:10002}.sm-bulkDialog.sm-nativeDialogLayer .sm-nativeDialogBackdrop{display:none}.sm-bulkDialog.sm-nativeDialogLayer .sm-nativeDialog{position:relative;left:auto;top:auto;transform:none;width:auto;box-sizing:border-box;background:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.25);padding:0;overflow:hidden;animation:sm-confirmPop .14s ease-out}.sm-bulkDialog .sm-nativeDialogHeader{padding:10px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#eee);flex:none;display:flex;align-items:center;gap:8px}.sm-bulkDialog .sm-nativeDialogTitle{margin:0;font-size:14px;line-height:20px;font-weight:600;text-align:center;width:100%}.sm-bulkDialog .sm-nativeDialogBody{padding:12px 16px;display:flex;flex-direction:column;gap:10px;font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary,#111)}.sm-bulkDialog .sm-nativeDialogFooter{padding:10px 16px;display:flex;flex-wrap:wrap;justify-content:space-between;gap:6px}.sm-bulkDialog .sm-nativeDialogButton{min-height:28px;padding:3px 14px;font-size:12px;line-height:20px;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;cursor:pointer;background:transparent;color:var(--dsw-alias-label-primary,#111);text-align:center}.sm-bulkDialog .sm-nativeDialogConfirm{background:var(--dsw-alias-accent-primary,#356ae6);color:#fff;border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-bulkDialog .sm-nativeDialogConfirm:hover:not(:disabled){filter:brightness(.94)}.sm-bulkDialog .sm-nativeDialogDanger{background:var(--dsw-alias-state-error-primary,#d92d20);color:#fff;border-color:var(--dsw-alias-state-error-primary,#d92d20)}.sm-bulkDialog .sm-nativeDialogDanger:hover:not(:disabled){filter:brightness(.92)}.sm-bulkDialog .sm-nativeDialog select.sm-viewSelect{font-size:13px;line-height:20px;height:32px;padding:5px 9px}",
|
|
513
|
+
".sm-bulkDialog .sm-nativeDialog{width:100%;max-height:min(640px,calc(100vh - 32px))}",
|
|
514
|
+
".sm-bulkDialogBody{display:flex;flex-direction:column;gap:12px;min-height:0}",
|
|
515
|
+
".sm-bulkDialogSummary{font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary,#111)}",
|
|
516
|
+
".sm-bulkDialogWarn{display:flex;align-items:flex-start;gap:8px;color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;background:rgba(217,80,80,.08);border:1px solid rgba(217,80,80,.25);border-radius:8px;padding:8px 10px}",
|
|
517
|
+
".sm-bulkSessionList{display:flex;flex-direction:column;gap:4px;max-height:200px;overflow:auto;border:1px solid var(--dsw-alias-border-l2,#eee);border-radius:8px;padding:6px}",
|
|
518
|
+
".sm-bulkSessionItem{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#666);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
519
|
+
".sm-bulkProgressBar{position:relative;height:8px;background:var(--dsw-alias-fill-l2,#eee);border-radius:4px;overflow:hidden}",
|
|
520
|
+
".sm-bulkProgressFill{position:absolute;top:0;left:0;bottom:0;background:var(--dsw-alias-accent-primary,#356ae6);transition:width .2s ease}",
|
|
521
|
+
".sm-bulkResultGroup{display:flex;flex-direction:column;gap:6px}",
|
|
522
|
+
".sm-bulkResultHeader{font-size:12px;line-height:18px;font-weight:500;color:var(--dsw-alias-label-secondary,#666)}",
|
|
523
|
+
".sm-bulkResultList{display:flex;flex-direction:column;gap:2px;max-height:160px;overflow:auto;border:1px solid var(--dsw-alias-border-l2,#eee);border-radius:8px;padding:6px}",
|
|
524
|
+
".sm-bulkResultItem{display:flex;flex-direction:column;gap:2px;padding:4px 6px;font-size:12px;line-height:18px;border-radius:6px;cursor:default}",
|
|
525
|
+
".sm-bulkResultItem:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
526
|
+
".sm-bulkResultItemTitle{color:var(--dsw-alias-label-primary,#111);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
527
|
+
".sm-bulkResultItemReason{color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px;word-break:break-word}",
|
|
528
|
+
".sm-bulkResultItemReasonSkip{color:var(--dsw-alias-label-tertiary,#888)}",
|
|
529
|
+
".sm-bulkResultFooter{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
530
|
+
".sm-footerBtn-wide{flex:1 1 auto;min-width:0;min-height:36px;justify-content:flex-start;padding:0 10px;border-radius:12px;font-size:13px;line-height:20px;gap:8px}",
|
|
531
|
+
".sm-footerBtn-wide:hover,.sm-footerBtn-wide:focus-visible{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
532
|
+
".sm-footerBtn-rail{width:36px;height:36px;padding:0;justify-content:center;border-radius:12px;color:var(--dsw-alias-label-secondary)}",
|
|
533
|
+
".sm-footerBtn-rail:hover,.sm-footerBtn-rail:focus-visible{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
534
|
+
".sm-footerBtn-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%}",
|
|
535
|
+
".sm-footer-wide{display:flex;width:100%}",
|
|
536
|
+
".sm-footer-rail{display:flex;width:100%;justify-content:center}",
|
|
537
|
+
"@media(max-width:720px){.sm-panelSelects{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.sm-panelDialog .sm-row{flex-wrap:wrap}.sm-panelDialog .sm-rowMain{flex-basis:100%}.sm-panelDialog .sm-rowActions{width:100%;min-width:0;flex:1 1 100%;justify-content:flex-start}}@media(max-width:540px){.sm-panelSelects{grid-template-columns:minmax(0,1fr)}}",
|
|
538
|
+
].join("");
|
|
539
|
+
|
|
540
|
+
// Auto-follow DSH's theme. We detect DSH's current theme from
|
|
541
|
+
// body[data-ds-dark-theme] / color-scheme / data-theme / class tokens,
|
|
542
|
+
// mirror it onto <html data-sm-theme>, and apply inline dialog colors.
|
|
543
|
+
// The dialogs use that attribute via the scoped CSS rules above, and
|
|
544
|
+
// inline styles guarantee an opaque background even when DSH's tokens
|
|
545
|
+
// resolve to `unset` / `transparent`.
|
|
546
|
+
const SM_THEME_ATTR = "data-sm-theme";
|
|
547
|
+
function smReadDshTheme() {
|
|
548
|
+
try {
|
|
549
|
+
const html = document.documentElement;
|
|
550
|
+
const body = document.body;
|
|
551
|
+
if (body && body.hasAttribute("data-ds-dark-theme")) return "dark";
|
|
552
|
+
const elements = [html, body].filter(Boolean);
|
|
553
|
+
const dark = /\b(dark|dim)\b/i;
|
|
554
|
+
const light = /\b(light|bright)\b/i;
|
|
555
|
+
for (const el of elements) {
|
|
556
|
+
const value = ["theme", "data-theme", "data-bs-theme", "color-scheme", "data-color-mode", "data-mode"]
|
|
557
|
+
.map((name) => String(el.getAttribute(name) || ""))
|
|
558
|
+
.join(" ");
|
|
559
|
+
if (dark.test(value)) return "dark";
|
|
560
|
+
if (light.test(value)) return "light";
|
|
561
|
+
}
|
|
562
|
+
for (const el of elements) {
|
|
563
|
+
const scheme = String(getComputedStyle(el).colorScheme || "").toLowerCase();
|
|
564
|
+
if (scheme.includes("dark")) return "dark";
|
|
565
|
+
if (scheme.includes("light")) return "light";
|
|
566
|
+
}
|
|
567
|
+
const bg = body ? getComputedStyle(body).backgroundColor : "";
|
|
568
|
+
const match = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
|
|
569
|
+
if (match) {
|
|
570
|
+
const luminance = (Number(match[1]) * 299 + Number(match[2]) * 587 + Number(match[3]) * 114) / 1000;
|
|
571
|
+
return luminance < 128 ? "dark" : "light";
|
|
572
|
+
}
|
|
573
|
+
} catch (_) { /* ignore */ }
|
|
574
|
+
return "light";
|
|
575
|
+
}
|
|
576
|
+
function smApplyInlineTheme(theme) {
|
|
577
|
+
try {
|
|
578
|
+
const values = theme === "dark"
|
|
579
|
+
? { background: "#1f1f23", color: "#ececec", border: "#3a3a40" }
|
|
580
|
+
: { background: "#fff", color: "#1f1f1f", border: "#e5e5e5" };
|
|
581
|
+
const roots = document.querySelectorAll(
|
|
582
|
+
".sm-panelDialog," +
|
|
583
|
+
".sm-confirmDialog.sm-nativeDialogLayer .sm-nativeDialog," +
|
|
584
|
+
".sm-migrateDialog.sm-nativeDialogLayer .sm-nativeDialog"
|
|
585
|
+
);
|
|
586
|
+
for (const el of roots) {
|
|
587
|
+
el.style.setProperty("background", values.background, "important");
|
|
588
|
+
el.style.setProperty("background-color", values.background, "important");
|
|
589
|
+
el.style.setProperty("color", values.color, "important");
|
|
590
|
+
el.style.setProperty("border-color", values.border, "important");
|
|
591
|
+
}
|
|
592
|
+
} catch (_) { /* ignore */ }
|
|
593
|
+
}
|
|
594
|
+
function smApplyTheme() {
|
|
595
|
+
try {
|
|
596
|
+
const theme = smReadDshTheme();
|
|
597
|
+
document.documentElement.setAttribute(SM_THEME_ATTR, theme);
|
|
598
|
+
smApplyInlineTheme(theme);
|
|
599
|
+
} catch (_) { /* ignore */ }
|
|
600
|
+
}
|
|
601
|
+
smApplyTheme();
|
|
602
|
+
if (typeof MutationObserver === "function") {
|
|
603
|
+
try {
|
|
604
|
+
const observer = new MutationObserver(() => smApplyTheme());
|
|
605
|
+
const options = { attributes: true, attributeFilter: ["class", "theme", "data-theme", "data-bs-theme", "data-ds-dark-theme", "data-color-mode", "data-mode", "color-scheme", "style"] };
|
|
606
|
+
if (document.documentElement) observer.observe(document.documentElement, options);
|
|
607
|
+
if (document.body) observer.observe(document.body, options);
|
|
608
|
+
observer.observe(document.body || document.documentElement, { childList: true, subtree: true });
|
|
609
|
+
} catch (_) { /* ignore */ }
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const CSS_ID = "dsh-session-manager/session-manager";
|
|
613
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(CSS_ID) + "]") === null) {
|
|
614
|
+
const tag = document.createElement("style");
|
|
615
|
+
tag.dataset.plugin = "dsh-session-manager";
|
|
616
|
+
tag.dataset.pluginCss = CSS_ID;
|
|
617
|
+
tag.textContent = css;
|
|
618
|
+
document.head.appendChild(tag);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
// Anchor the session manager panel to the sidebar right edge so it
|
|
623
|
+
// always fits within the sidebar area, regardless of viewport size.
|
|
624
|
+
const SM_PANEL_CSS_ID = "dsh-session-manager/session-manager-anchor";
|
|
625
|
+
const installAnchor = () => {
|
|
626
|
+
if (typeof document === "undefined") return;
|
|
627
|
+
if (document.querySelector("style[data-plugin-css=" + JSON.stringify(SM_PANEL_CSS_ID) + "]") !== null) return;
|
|
628
|
+
const tag = document.createElement("style");
|
|
629
|
+
tag.dataset.plugin = "dsh-session-manager";
|
|
630
|
+
tag.dataset.pluginCss = SM_PANEL_CSS_ID;
|
|
631
|
+
tag.textContent = ":root{--sm-panel-left:8px}";
|
|
632
|
+
document.head.appendChild(tag);
|
|
633
|
+
const updateLeft = () => {
|
|
634
|
+
let right = 8;
|
|
635
|
+
const candidates = [
|
|
636
|
+
"[data-dsh-sidebar]",
|
|
637
|
+
"[data-testid=sidebar]",
|
|
638
|
+
"[aria-label=会话][role=navigation]",
|
|
639
|
+
"aside",
|
|
640
|
+
".dsw-sidebar"
|
|
641
|
+
];
|
|
642
|
+
for (const sel of candidates) {
|
|
643
|
+
const el = document.querySelector(sel);
|
|
644
|
+
if (el !== null) {
|
|
645
|
+
const rect = el.getBoundingClientRect();
|
|
646
|
+
if (rect.right > 100 && rect.right < window.innerWidth - 40) {
|
|
647
|
+
right = Math.round(rect.right) + 8;
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
document.documentElement.style.setProperty("--sm-panel-left", right + "px");
|
|
653
|
+
};
|
|
654
|
+
updateLeft();
|
|
655
|
+
window.addEventListener("resize", updateLeft);
|
|
656
|
+
};
|
|
657
|
+
try { installAnchor(); } catch (_) {}
|
|
658
|
+
|
|
659
|
+
const h = (type, props, ...children) => React.createElement(type, props, ...children);
|
|
660
|
+
const callApi = (path, payload) => fetch(API + path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload || {}) }).then((r) => r.json());
|
|
661
|
+
const fetchApi = (path, options) => fetch(API + path, options).then((r) => r.json());
|
|
662
|
+
// Run a bulk action across many sessionIds. /batch already does per-id
|
|
663
|
+
// error reporting, so partial failures come back as {summary, items}
|
|
664
|
+
// with per-id success/failed/skipped status. Cancellation is best-effort
|
|
665
|
+
// because the v0.1.7 host does not reliably support req.on("close").
|
|
666
|
+
async function runBatch(args, onProgress) {
|
|
667
|
+
const { action, sessionIds, payload } = args || {};
|
|
668
|
+
const controller = new AbortController();
|
|
669
|
+
try {
|
|
670
|
+
const response = await fetch(API + "/batch", {
|
|
671
|
+
method: "POST",
|
|
672
|
+
headers: { "content-type": "application/json" },
|
|
673
|
+
body: JSON.stringify({ action, sessionIds: sessionIds || [], payload: payload || {} }),
|
|
674
|
+
signal: controller.signal
|
|
675
|
+
});
|
|
676
|
+
const data = await response.json();
|
|
677
|
+
if (data && data.ok) {
|
|
678
|
+
const result = data.result || {};
|
|
679
|
+
const items = Array.isArray(result.items) ? result.items : [];
|
|
680
|
+
if (typeof onProgress === "function") onProgress({ done: items.length, total: (sessionIds || []).length, lastItem: items[items.length - 1] || null });
|
|
681
|
+
return { summary: result.summary || { total: items.length, success: 0, failed: 0, skipped: 0 }, items, aborted: !!result.aborted };
|
|
682
|
+
}
|
|
683
|
+
const error = new Error((data && data.error) || "batch failed");
|
|
684
|
+
error.code = (data && data.code) || "bad-request";
|
|
685
|
+
throw error;
|
|
686
|
+
} catch (error) {
|
|
687
|
+
if (error && error.name === "AbortError") return { summary: { total: 0, success: 0, failed: 0, skipped: 0 }, items: [], aborted: true };
|
|
688
|
+
throw error;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const EMPTY_ANNOTATION = Object.freeze({ favorite: false, reviewLater: false, tags: Object.freeze([]), note: "", priority: 3, revision: 0 });
|
|
693
|
+
const validAnnotation = value => value && typeof value.favorite === "boolean" && typeof value.reviewLater === "boolean" && Array.isArray(value.tags) && value.tags.every(tag => typeof tag === "string") && typeof value.note === "string" && (value.priority === null || Number.isInteger(value.priority) && value.priority >= 1 && value.priority <= 5) && Number.isSafeInteger(value.revision) && value.revision >= 0;
|
|
694
|
+
const annotationOf = (state, id) => Object.hasOwn(state.entries, id) ? state.entries[id] : EMPTY_ANNOTATION;
|
|
695
|
+
const annotationStore = (() => {
|
|
696
|
+
let state = { entries: {}, loaded: false, loading: false, error: null, pending: new Set() };
|
|
697
|
+
const listeners = new Set();
|
|
698
|
+
let tail = Promise.resolve(); let loading; let refreshAgain = false;
|
|
699
|
+
const publish = patch => { state = { ...state, ...patch }; for (const listener of [...listeners]) listener(); };
|
|
700
|
+
const enqueue = operation => { const result = tail.then(operation); tail = result.catch(() => {}); return result; };
|
|
701
|
+
const apiError = data => Object.assign(new Error(data?.error || "Annotation request failed"), { code: data?.code });
|
|
702
|
+
const store = {
|
|
703
|
+
notify: null,
|
|
704
|
+
getSnapshot: () => state,
|
|
705
|
+
subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
|
|
706
|
+
hasSubscribers: () => listeners.size > 0,
|
|
707
|
+
load(force = false) {
|
|
708
|
+
if (loading) { if (force) refreshAgain = true; return loading; }
|
|
709
|
+
if (state.loaded && !force) return Promise.resolve();
|
|
710
|
+
publish({ loading: true, error: null });
|
|
711
|
+
loading = enqueue(async () => {
|
|
712
|
+
const data = await fetchApi("/annotations", { cache: "no-store" });
|
|
713
|
+
if (!data?.ok) throw apiError(data);
|
|
714
|
+
const entries = data.result?.annotations;
|
|
715
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries)
|
|
716
|
+
|| Object.values(entries).some(value => !validAnnotation(value))) throw new Error("Invalid annotation response");
|
|
717
|
+
publish({ entries, loaded: true, error: null });
|
|
718
|
+
}).catch(error => { publish({ error: error instanceof Error ? error.message : String(error) }); })
|
|
719
|
+
.finally(() => { loading = null; publish({ loading: false }); if (refreshAgain) { refreshAgain = false; void store.load(true); } });
|
|
720
|
+
return loading;
|
|
721
|
+
},
|
|
722
|
+
save(id, patch, expectedRevision) {
|
|
723
|
+
if (state.pending.has(id)) return Promise.reject(new Error("This session's annotations are being saved"));
|
|
724
|
+
if (!state.loaded || state.error) return Promise.reject(new Error(state.error || "Annotations have not loaded yet"));
|
|
725
|
+
publish({ pending: new Set([...state.pending, id]) });
|
|
726
|
+
return enqueue(async () => {
|
|
727
|
+
const data = await callApi("/annotations", { sessionId: id, patch, ...(expectedRevision !== undefined ? { expectedRevision } : {}) });
|
|
728
|
+
if (!data?.ok) throw apiError(data);
|
|
729
|
+
const annotation = data.result?.annotation;
|
|
730
|
+
if (!validAnnotation(annotation)) throw new Error("Invalid annotation response");
|
|
731
|
+
publish({ entries: { ...state.entries, [id]: annotation } });
|
|
732
|
+
try { store.notify?.(); } catch (_) { /* cross-tab sync is best-effort */ }
|
|
733
|
+
return annotation;
|
|
734
|
+
}).finally(() => { const pending = new Set(state.pending); pending.delete(id); publish({ pending }); });
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
return store;
|
|
738
|
+
})();
|
|
739
|
+
const useAnnotations = (refreshOnMount = false) => {
|
|
740
|
+
const state = useSyncExternalStore(annotationStore.subscribe, annotationStore.getSnapshot, annotationStore.getSnapshot);
|
|
741
|
+
useEffect(() => { void annotationStore.load(refreshOnMount); }, [refreshOnMount]);
|
|
742
|
+
return state;
|
|
743
|
+
};
|
|
744
|
+
const annotationButtons = (id, title, t, state, className, onToggle, onEdit, disabled = false, editorOpen = false) => {
|
|
745
|
+
const value = annotationOf(state, id);
|
|
746
|
+
const busy = disabled || !state.loaded || !!state.error || state.pending.has(id);
|
|
747
|
+
const favoriteLabel = t(value.favorite ? "marks.favorite.remove" : "marks.favorite.add") + ": " + title;
|
|
748
|
+
const reviewLabel = t(value.reviewLater ? "marks.review.remove" : "marks.review.add") + ": " + title;
|
|
749
|
+
return h(React.Fragment, null,
|
|
750
|
+
h("button", { type: "button", className: className + " sm-markIcon" + (value.favorite ? " sm-markOn" : ""), disabled: busy, "aria-label": favoriteLabel, title: favoriteLabel, "aria-pressed": value.favorite, onClick: () => onToggle({ favorite: !value.favorite }) }, value.favorite ? "★" : "☆"),
|
|
751
|
+
h("button", { type: "button", className: className + " sm-markIcon" + (value.reviewLater ? " sm-markOn" : ""), disabled: busy, "aria-label": reviewLabel, title: reviewLabel, "aria-pressed": value.reviewLater, onClick: () => onToggle({ reviewLater: !value.reviewLater }) }, "◷"),
|
|
752
|
+
h("button", { type: "button", className: className + (editorOpen ? " sm-annotationToggleOpen sm-headerBtnActive" : ""), disabled: busy, "aria-label": t("marks.edit.aria") + ": " + title, title: t("marks.edit.aria"), ...(className === "sm-headerBtn" ? { "aria-expanded": editorOpen } : {}), onClick: e => onEdit(e) }, t(className === "sm-headerBtn" ? "marks.header.edit" : "marks.edit"), className === "sm-headerBtn" ? h("span", { className: "sm-priorityBadge sm-priority-" + (value.priority ?? 3), title: t("marks.priority." + (value.priority ?? 3)) }, "P" + (value.priority ?? 3)) : null)
|
|
753
|
+
);
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
// Inline clipboard parser mirroring lib/clipboard-parser.js. The server
|
|
758
|
+
// re-validates the patch via annotationStore.save, so this helper only
|
|
759
|
+
// needs to be permissive enough to surface obvious mistakes early.
|
|
760
|
+
const TAG_LIMITS = { tags: 20, tagLength: 32, noteLength: 2000 };
|
|
761
|
+
const TAG_REJECT_RE = /[,,\x00-\x1f]/;
|
|
762
|
+
// Recover the most useful JSON object from a possibly noisy
|
|
763
|
+
// clipboard string. Tries, in order:
|
|
764
|
+
// 1. JSON.parse on the trimmed text.
|
|
765
|
+
// 2. JSON.parse inside a Markdown \`\`\`json ... \`\`\` fence.
|
|
766
|
+
// 3. JSON.parse on the first balanced {...} the scanner finds.
|
|
767
|
+
// 4. JSON.parse after repairing common AI mistakes
|
|
768
|
+
// (smart quotes, full-width punctuation, stray backslashes,
|
|
769
|
+
// trailing commas).
|
|
770
|
+
// Every failing attempt appends the underlying error so the user can
|
|
771
|
+
// see what actually went wrong.
|
|
772
|
+
const parseImportedAnnotation = raw => {
|
|
773
|
+
if (typeof raw !== "string" || raw.trim() === "") return { ok: false, error: "剪贴板为空 / Clipboard is empty" };
|
|
774
|
+
let work = raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw;
|
|
775
|
+
work = work.replace(/^\s+|\s+$/g, "");
|
|
776
|
+
const fence = work.match(/^```(?:json|JSON)?\s*([\s\S]*?)\s*```$/);
|
|
777
|
+
const scanForObject = text => {
|
|
778
|
+
const len = text.length;
|
|
779
|
+
for (let i = 0; i < len; i++) {
|
|
780
|
+
if (text[i] !== "{") continue;
|
|
781
|
+
let depth = 0, inString = false, escape = false;
|
|
782
|
+
for (let j = i; j < len; j++) {
|
|
783
|
+
const c = text[j];
|
|
784
|
+
if (inString) {
|
|
785
|
+
if (escape) escape = false;
|
|
786
|
+
else if (c === String.fromCharCode(92)) escape = true;
|
|
787
|
+
else if (c === String.fromCharCode(34)) inString = false;
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
if (c === String.fromCharCode(34)) inString = true;
|
|
791
|
+
else if (c === "{") depth++;
|
|
792
|
+
else if (c === "}") { depth--; if (depth === 0) return text.slice(i, j + 1); }
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return null;
|
|
796
|
+
};
|
|
797
|
+
const repairLooseJson = text => text
|
|
798
|
+
// Smart quotes from Chinese / English typography.
|
|
799
|
+
.replace(/[“”]/g, "\"")
|
|
800
|
+
.replace(/[‘’]/g, "'")
|
|
801
|
+
// Stray single backslashes that aren't part of a valid JSON escape.
|
|
802
|
+
// Walk the string char-by-char to avoid touching \", \, \/, \n etc.
|
|
803
|
+
.replace(/\\(?![\\"/bfnrtu])/g, "\\\\");
|
|
804
|
+
const attempts = [];
|
|
805
|
+
const tryParse = (label, value) => {
|
|
806
|
+
try { return JSON.parse(value); }
|
|
807
|
+
catch (error) { attempts.push(label + ": " + error.message); return null; }
|
|
808
|
+
};
|
|
809
|
+
let parsed;
|
|
810
|
+
const primary = fence ? fence[1] : work;
|
|
811
|
+
parsed = tryParse("原始", primary) || tryParse("修复引号/反斜杠", repairLooseJson(primary));
|
|
812
|
+
if (parsed === null) {
|
|
813
|
+
const scanned = scanForObject(work);
|
|
814
|
+
if (scanned !== null) {
|
|
815
|
+
parsed = tryParse("扫描对象", scanned) || tryParse("扫描对象+修复", repairLooseJson(scanned));
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (parsed === null) {
|
|
819
|
+
return { ok: false, error: "剪贴板中的 JSON 无法解析 — " + attempts.join(";") };
|
|
820
|
+
}
|
|
821
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
822
|
+
return { ok: false, error: "剪贴板内容不是 JSON 对象" };
|
|
823
|
+
}
|
|
824
|
+
const allowed = new Set(["tags", "note", "priority"]);
|
|
825
|
+
const warnings = [];
|
|
826
|
+
for (const key of Object.keys(parsed)) if (!allowed.has(key)) warnings.push({ field: key, kind: "ignored" });
|
|
827
|
+
let tags = [];
|
|
828
|
+
if ("tags" in parsed) {
|
|
829
|
+
if (!Array.isArray(parsed.tags)) return { ok: false, error: "tags must be an array of strings" };
|
|
830
|
+
const seen = new Set();
|
|
831
|
+
for (const item of parsed.tags) {
|
|
832
|
+
if (typeof item !== "string") { warnings.push({ field: "tags", kind: "rejected" }); continue; }
|
|
833
|
+
const tag = item.trim();
|
|
834
|
+
if (!tag) continue;
|
|
835
|
+
if (tag.length > TAG_LIMITS.tagLength || TAG_REJECT_RE.test(tag)) {
|
|
836
|
+
warnings.push({ field: "tags", kind: "rejected", value: tag });
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
const key = tag.toLowerCase();
|
|
840
|
+
if (seen.has(key)) continue;
|
|
841
|
+
seen.add(key);
|
|
842
|
+
if (tags.length >= TAG_LIMITS.tags) { warnings.push({ field: "tags", kind: "truncated" }); continue; }
|
|
843
|
+
tags.push(tag);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
let note = "";
|
|
847
|
+
if ("note" in parsed) {
|
|
848
|
+
if (typeof parsed.note !== "string") return { ok: false, error: "note must be a string" };
|
|
849
|
+
if (parsed.note.includes(String.fromCharCode(0))) return { ok: false, error: "note must not contain NUL characters" };
|
|
850
|
+
if (parsed.note.length > TAG_LIMITS.noteLength) {
|
|
851
|
+
warnings.push({ field: "note", kind: "truncated", from: parsed.note.length, to: TAG_LIMITS.noteLength });
|
|
852
|
+
note = parsed.note.slice(0, TAG_LIMITS.noteLength);
|
|
853
|
+
} else note = parsed.note;
|
|
854
|
+
}
|
|
855
|
+
let priority;
|
|
856
|
+
if ("priority" in parsed) {
|
|
857
|
+
// AI-returned null is treated as the default priority (3 = Normal) so
|
|
858
|
+
// the UI never has to special-case unset entries.
|
|
859
|
+
if (parsed.priority === null) priority = 3;
|
|
860
|
+
else if (Number.isInteger(parsed.priority) && parsed.priority >= 1 && parsed.priority <= 5) priority = parsed.priority;
|
|
861
|
+
else warnings.push({ field: "priority", kind: "rejected", value: parsed.priority });
|
|
862
|
+
}
|
|
863
|
+
const annotation = { tags, note };
|
|
864
|
+
if (priority !== undefined) annotation.priority = priority;
|
|
865
|
+
return { ok: true, annotation, warnings };
|
|
866
|
+
};
|
|
867
|
+
const CLIPBOARD_PROMPT_TEXT = {
|
|
868
|
+
zh: [
|
|
869
|
+
"请根据当前对话内容,为这个会话生成整理标记。",
|
|
870
|
+
"",
|
|
871
|
+
"要求:",
|
|
872
|
+
"1. 生成最多 20 个标签,每个标签最多 32 个字符。",
|
|
873
|
+
"2. 标签应简洁、具体,便于之后搜索。",
|
|
874
|
+
"3. 标签不要重复,不要生成无意义的标签。",
|
|
875
|
+
"4. 生成一段不超过 2000 个字符的备注,总结本次对话的主题、结论、未完成事项或下一步行动。",
|
|
876
|
+
"5. 不要编造对话中没有出现的信息。",
|
|
877
|
+
"6. 只返回合法 JSON,不要使用 Markdown 代码块,不要附加解释。",
|
|
878
|
+
"",
|
|
879
|
+
"返回格式:",
|
|
880
|
+
"{",
|
|
881
|
+
' "tags": ["标签1", "标签2"],',
|
|
882
|
+
' "note": "会话备注"',
|
|
883
|
+
"}"
|
|
884
|
+
].join(String.fromCharCode(10)),
|
|
885
|
+
en: [
|
|
886
|
+
"Summarize this conversation into session annotations.",
|
|
887
|
+
"",
|
|
888
|
+
"Requirements:",
|
|
889
|
+
"1. Up to 20 tags, at most 32 characters each.",
|
|
890
|
+
"2. Tags should be concise and help future searches.",
|
|
891
|
+
"3. Do not repeat tags or invent meaningless ones.",
|
|
892
|
+
"4. Write a note under 2000 characters summarizing the topic, conclusions, outstanding items and next steps.",
|
|
893
|
+
"5. Do not fabricate details that are not present in the conversation.",
|
|
894
|
+
"6. Return only valid JSON, no Markdown fences, no extra commentary.",
|
|
895
|
+
"",
|
|
896
|
+
"Return shape:",
|
|
897
|
+
"{",
|
|
898
|
+
' "tags": ["tag1", "tag2"],',
|
|
899
|
+
' "note": "session note"',
|
|
900
|
+
"}"
|
|
901
|
+
].join(String.fromCharCode(10))
|
|
902
|
+
};
|
|
903
|
+
const promptFor = lang => CLIPBOARD_PROMPT_TEXT[lang] || CLIPBOARD_PROMPT_TEXT.zh;
|
|
904
|
+
function BatchPreviewDialog(props) {
|
|
905
|
+
const t = typeof props.t === "function" ? props.t : (key) => key;
|
|
906
|
+
const open = props.open === true;
|
|
907
|
+
const onCancel = typeof props.onCancel === "function" ? props.onCancel : () => {};
|
|
908
|
+
const onExecute = typeof props.onExecute === "function" ? props.onExecute : () => {};
|
|
909
|
+
const actionLabel = props.actionLabel || "";
|
|
910
|
+
const sessions = Array.isArray(props.sessions) ? props.sessions : [];
|
|
911
|
+
const warning = typeof props.warning === "string" ? props.warning : "";
|
|
912
|
+
const confirmLabel = props.confirmLabel || t("bulk.dialog.execute");
|
|
913
|
+
const cancelLabel = props.cancelLabel || t("bulk.dialog.cancel");
|
|
914
|
+
const danger = props.danger === true;
|
|
915
|
+
const [expanded, setExpanded] = useState(false);
|
|
916
|
+
const [zIndex] = useState(() => open ? nextDialogZ() : 0);
|
|
917
|
+
const escStateRef = useRef({});
|
|
918
|
+
escStateRef.current = { onCancel };
|
|
919
|
+
useEffect(() => {
|
|
920
|
+
if (!open) return;
|
|
921
|
+
const handler = (e) => { if (e.key !== "Escape" || e.defaultPrevented) return; e.preventDefault(); e.stopPropagation(); escStateRef.current.onCancel(); };
|
|
922
|
+
window.addEventListener("keydown", handler);
|
|
923
|
+
return () => window.removeEventListener("keydown", handler);
|
|
924
|
+
}, [open]);
|
|
925
|
+
if (!open) return null;
|
|
926
|
+
const showExpand = sessions.length > 12;
|
|
927
|
+
const visible = expanded ? sessions : sessions.slice(0, 12);
|
|
928
|
+
return h("div", { className: "sm-nativeDialogLayer sm-bulkDialog", role: "presentation", style: { zIndex: zIndex || undefined } },
|
|
929
|
+
h("section", { className: "sm-nativeDialog", role: "dialog", "aria-modal": "true", "aria-labelledby": "sm-batch-preview-title" },
|
|
930
|
+
h("div", { className: "sm-nativeDialogHeader" }, h("h2", { id: "sm-batch-preview-title", className: "sm-nativeDialogTitle" }, t("bulk.dialog.title", { action: actionLabel }))),
|
|
931
|
+
h("div", { className: "sm-nativeDialogBody" },
|
|
932
|
+
h("div", { className: "sm-bulkDialogSummary" }, t("bulk.dialog.summary", { count: sessions.length, action: actionLabel })),
|
|
933
|
+
warning ? h("div", { className: "sm-bulkDialogWarn" }, warning) : null,
|
|
934
|
+
h("div", { className: "sm-bulkSessionList" },
|
|
935
|
+
visible.map((s, i) => h("div", { key: (s.id || i) + "-" + i, className: "sm-bulkSessionItem", title: s.id || "" }, s.displayTitle || s.id || "—"))
|
|
936
|
+
),
|
|
937
|
+
showExpand ? h("button", { type: "button", className: "sm-resetFilters", onClick: () => setExpanded(value => !value) }, expanded ? t("panel.reset") : "…(+" + (sessions.length - 12) + ")") : null
|
|
938
|
+
),
|
|
939
|
+
h("div", { className: "sm-nativeDialogFooter" },
|
|
940
|
+
h("button", { type: "button", className: "sm-nativeDialogButton sm-nativeDialogCancel", onClick: onCancel }, cancelLabel),
|
|
941
|
+
h("button", { type: "button", className: "sm-nativeDialogButton" + (danger ? " sm-nativeDialogDanger" : " sm-nativeDialogConfirm"), onClick: onExecute }, confirmLabel)
|
|
942
|
+
)
|
|
943
|
+
)
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function BatchProgressDialog(props) {
|
|
948
|
+
const t = typeof props.t === "function" ? props.t : (key) => key;
|
|
949
|
+
const open = props.open === true;
|
|
950
|
+
const [zIndex] = useState(() => open ? nextDialogZ() : 0);
|
|
951
|
+
const actionLabel = props.actionLabel || "";
|
|
952
|
+
const done = Number.isInteger(props.done) ? props.done : 0;
|
|
953
|
+
const total = Number.isInteger(props.total) ? props.total : 0;
|
|
954
|
+
const completed = props.completed === true;
|
|
955
|
+
const onCancel = typeof props.onCancel === "function" ? props.onCancel : () => {};
|
|
956
|
+
const cancelLabel = props.cancelLabel || t("bulk.progress.cancel");
|
|
957
|
+
if (!open) return null;
|
|
958
|
+
const pct = total > 0 ? Math.max(0, Math.min(100, Math.round((done / total) * 100))) : 0;
|
|
959
|
+
return h("div", { className: "sm-nativeDialogLayer sm-bulkDialog", role: "presentation", style: { zIndex: zIndex || undefined } },
|
|
960
|
+
h("section", { className: "sm-nativeDialog", role: "dialog", "aria-modal": "true", "aria-labelledby": "sm-batch-progress-title" },
|
|
961
|
+
h("div", { className: "sm-nativeDialogHeader" }, h("h2", { id: "sm-batch-progress-title", className: "sm-nativeDialogTitle" }, t("bulk.progress.title", { action: actionLabel }))),
|
|
962
|
+
h("div", { className: "sm-nativeDialogBody" },
|
|
963
|
+
h("div", { className: "sm-bulkDialogSummary" }, completed ? t("bulk.progress.completed", { done, total }) : t("bulk.progress.current", { done, total })),
|
|
964
|
+
h("div", { className: "sm-bulkProgressBar", role: "progressbar", "aria-valuemin": 0, "aria-valuemax": total, "aria-valuenow": done },
|
|
965
|
+
h("div", { className: "sm-bulkProgressFill", style: { width: pct + "%" } })
|
|
966
|
+
)
|
|
967
|
+
),
|
|
968
|
+
h("div", { className: "sm-nativeDialogFooter" },
|
|
969
|
+
h("button", { type: "button", className: "sm-nativeDialogButton sm-nativeDialogCancel", disabled: completed, onClick: onCancel }, cancelLabel)
|
|
970
|
+
)
|
|
971
|
+
)
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function BatchResultDialog(props) {
|
|
976
|
+
const t = typeof props.t === "function" ? props.t : (key) => key;
|
|
977
|
+
const open = props.open === true;
|
|
978
|
+
const [zIndex] = useState(() => open ? nextDialogZ() : 0);
|
|
979
|
+
const actionLabel = props.actionLabel || "";
|
|
980
|
+
const items = Array.isArray(props.items) ? props.items : [];
|
|
981
|
+
const onClose = typeof props.onClose === "function" ? props.onClose : () => {};
|
|
982
|
+
const onRetry = typeof props.onRetry === "function" ? props.onRetry : () => {};
|
|
983
|
+
const closeLabel = props.closeLabel || t("bulk.result.close");
|
|
984
|
+
const retryLabel = props.retryLabel || t("bulk.result.retry");
|
|
985
|
+
const escStateRef = useRef({});
|
|
986
|
+
escStateRef.current = { onClose };
|
|
987
|
+
useEffect(() => {
|
|
988
|
+
if (!open) return;
|
|
989
|
+
const handler = (e) => { if (e.key !== "Escape" || e.defaultPrevented) return; e.preventDefault(); e.stopPropagation(); escStateRef.current.onClose(); };
|
|
990
|
+
window.addEventListener("keydown", handler);
|
|
991
|
+
return () => window.removeEventListener("keydown", handler);
|
|
992
|
+
}, [open]);
|
|
993
|
+
if (!open) return null;
|
|
994
|
+
const groups = { success: [], failed: [], skipped: [] };
|
|
995
|
+
for (const item of items) {
|
|
996
|
+
const status = item && item.status;
|
|
997
|
+
if (status === "success") groups.success.push(item);
|
|
998
|
+
else if (status === "skipped") groups.skipped.push(item);
|
|
999
|
+
else groups.failed.push(item);
|
|
1000
|
+
}
|
|
1001
|
+
const renderGroup = (labelKey, group, reasonClass) => {
|
|
1002
|
+
if (group.length === 0) return null;
|
|
1003
|
+
return h("div", { key: labelKey, className: "sm-bulkResultGroup" },
|
|
1004
|
+
h("div", { className: "sm-bulkResultHeader" }, t(labelKey, { count: group.length })),
|
|
1005
|
+
h("div", { className: "sm-bulkResultList" },
|
|
1006
|
+
group.map((item, i) => h("div", { key: (item.sessionId || i) + "-" + i, className: "sm-bulkResultItem", title: item.sessionId || "" },
|
|
1007
|
+
h("div", { className: "sm-bulkResultItemTitle" }, item.displayTitle || item.sessionId || "—"),
|
|
1008
|
+
reasonClass && (item.reason || item.error) ? h("div", { className: reasonClass }, item.reason || item.error) : null
|
|
1009
|
+
))
|
|
1010
|
+
)
|
|
1011
|
+
);
|
|
1012
|
+
};
|
|
1013
|
+
return h("div", { className: "sm-nativeDialogLayer sm-bulkDialog", role: "presentation", style: { zIndex: zIndex || undefined } },
|
|
1014
|
+
h("section", { className: "sm-nativeDialog", role: "dialog", "aria-modal": "true", "aria-labelledby": "sm-batch-result-title" },
|
|
1015
|
+
h("div", { className: "sm-nativeDialogHeader" }, h("h2", { id: "sm-batch-result-title", className: "sm-nativeDialogTitle" }, t("bulk.result.title", { action: actionLabel }))),
|
|
1016
|
+
h("div", { className: "sm-nativeDialogBody" },
|
|
1017
|
+
renderGroup("bulk.result.successGroup", groups.success),
|
|
1018
|
+
renderGroup("bulk.result.failedGroup", groups.failed, "sm-bulkResultItemReason"),
|
|
1019
|
+
renderGroup("bulk.result.skippedGroup", groups.skipped, "sm-bulkResultItemReasonSkip")
|
|
1020
|
+
),
|
|
1021
|
+
h("div", { className: "sm-nativeDialogFooter" },
|
|
1022
|
+
h("button", { type: "button", className: "sm-nativeDialogButton sm-nativeDialogCancel", onClick: onClose }, closeLabel),
|
|
1023
|
+
groups.failed.length > 0 ? h("button", { type: "button", className: "sm-nativeDialogButton sm-nativeDialogConfirm", onClick: () => onRetry(groups.failed.map(item => item.sessionId)) }, retryLabel) : null
|
|
1024
|
+
)
|
|
1025
|
+
)
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function BulkActionBar(props) {
|
|
1030
|
+
const t = props.t;
|
|
1031
|
+
const selectedCount = props.selectedCount || 0;
|
|
1032
|
+
const filteredCount = props.filteredCount || 0;
|
|
1033
|
+
const busy = props.busy === true;
|
|
1034
|
+
const onAction = props.onAction;
|
|
1035
|
+
const onOpenTagDialog = props.onOpenTagDialog;
|
|
1036
|
+
const onOpenPriorityDialog = props.onOpenPriorityDialog;
|
|
1037
|
+
const onOpenMoveDialog = props.onOpenMoveDialog;
|
|
1038
|
+
const onOpenPresetDialog = props.onOpenPresetDialog;
|
|
1039
|
+
const disableNonDestructive = selectedCount === 0 || busy;
|
|
1040
|
+
const destructiveBusy = selectedCount === 0 || busy;
|
|
1041
|
+
return h("div", { className: "sm-bulkBar", role: "toolbar", "aria-label": t("bulk.heading") },
|
|
1042
|
+
h("span", { className: "sm-bulkSummary" }, t("bulk.selectedFiltered", { selected: selectedCount, filtered: filteredCount })),
|
|
1043
|
+
h("button", { type: "button", className: "sm-bulkBtn", disabled: disableNonDestructive, onClick: () => onAction("archive") }, t("bulk.archive")),
|
|
1044
|
+
h("button", { type: "button", className: "sm-bulkBtn", disabled: disableNonDestructive, onClick: () => onAction("unarchive") }, t("bulk.unarchive")),
|
|
1045
|
+
h("button", { type: "button", className: "sm-bulkBtn", disabled: disableNonDestructive, onClick: () => onAction("favorite") }, t("bulk.favorite")),
|
|
1046
|
+
h("button", { type: "button", className: "sm-bulkBtn", disabled: disableNonDestructive, onClick: () => onAction("unfavorite") }, t("bulk.unfavorite")),
|
|
1047
|
+
h("button", { type: "button", className: "sm-bulkBtn", disabled: disableNonDestructive, onClick: () => onAction("review") }, t("bulk.review")),
|
|
1048
|
+
h("button", { type: "button", className: "sm-bulkBtn", disabled: disableNonDestructive, onClick: () => onAction("unreview") }, t("bulk.unreview")),
|
|
1049
|
+
// Row 2 of the 3x6 grid (添加标签 / 清空标签 / 设置优先级 / 移动至工作区 / 迁移预设 / 删除会话).
|
|
1050
|
+
h("button", { type: "button", className: "sm-bulkBtn", disabled: disableNonDestructive, onClick: onOpenTagDialog }, t("bulk.tagsAdd")),
|
|
1051
|
+
h("button", { type: "button", className: "sm-bulkBtn", disabled: disableNonDestructive, onClick: () => onAction("remove-tags", { tags: [] }) }, t("bulk.tagsRemove")),
|
|
1052
|
+
h("button", { type: "button", className: "sm-bulkBtn", disabled: disableNonDestructive, onClick: onOpenPriorityDialog }, t("bulk.priority")),
|
|
1053
|
+
h("button", { type: "button", className: "sm-bulkBtn", disabled: disableNonDestructive, onClick: onOpenMoveDialog }, t("bulk.moveBtn")),
|
|
1054
|
+
h("button", { type: "button", className: "sm-bulkBtn", disabled: disableNonDestructive, onClick: onOpenPresetDialog }, t("bulk.presetMigrate")),
|
|
1055
|
+
h("button", { type: "button", className: "sm-bulkBtn sm-bulkBtnDanger", disabled: destructiveBusy, onClick: () => onAction("delete", null, true) }, t("bulk.delete"))
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function BulkTagInputDialog(props) {
|
|
1060
|
+
const t = typeof props.t === "function" ? props.t : (key) => key;
|
|
1061
|
+
const open = props.open === true;
|
|
1062
|
+
const [zIndex] = useState(() => open ? nextDialogZ() : 0);
|
|
1063
|
+
const title = props.title || t("bulk.tagsAdd");
|
|
1064
|
+
const placeholder = props.placeholder || t("bulk.tags.placeholder");
|
|
1065
|
+
const onCancel = typeof props.onCancel === "function" ? props.onCancel : () => {};
|
|
1066
|
+
const onApply = typeof props.onApply === "function" ? props.onApply : () => {};
|
|
1067
|
+
const confirmLabel = props.confirmLabel || t("bulk.confirm.tagsAdd");
|
|
1068
|
+
const [value, setValue] = useState("");
|
|
1069
|
+
useEffect(() => { if (open) setValue(""); }, [open]);
|
|
1070
|
+
const escStateRef = useRef({});
|
|
1071
|
+
escStateRef.current = { onCancel };
|
|
1072
|
+
useEffect(() => {
|
|
1073
|
+
if (!open) return;
|
|
1074
|
+
const handler = (e) => { if (e.key !== "Escape" || e.defaultPrevented) return; e.preventDefault(); e.stopPropagation(); escStateRef.current.onCancel(); };
|
|
1075
|
+
window.addEventListener("keydown", handler);
|
|
1076
|
+
return () => window.removeEventListener("keydown", handler);
|
|
1077
|
+
}, [open]);
|
|
1078
|
+
if (!open) return null;
|
|
1079
|
+
const submit = () => {
|
|
1080
|
+
const tags = (value || "").split(/[,,\n]/).map(s => s.trim()).filter(Boolean);
|
|
1081
|
+
onApply(tags);
|
|
1082
|
+
};
|
|
1083
|
+
return h("div", { className: "sm-nativeDialogLayer sm-bulkDialog", role: "presentation", style: { zIndex: zIndex || undefined } },
|
|
1084
|
+
h("section", { className: "sm-nativeDialog", role: "dialog", "aria-modal": "true", "aria-labelledby": "sm-bulk-tags-title" },
|
|
1085
|
+
h("div", { className: "sm-nativeDialogHeader" }, h("h2", { id: "sm-bulk-tags-title", className: "sm-nativeDialogTitle" }, title)),
|
|
1086
|
+
h("div", { className: "sm-nativeDialogBody" },
|
|
1087
|
+
h("textarea", { className: "sm-noteInput", "aria-label": placeholder, value, placeholder, rows: 3, onChange: (e) => setValue(e.target.value), autoFocus: true })
|
|
1088
|
+
),
|
|
1089
|
+
h("div", { className: "sm-nativeDialogFooter" },
|
|
1090
|
+
h("button", { type: "button", className: "sm-nativeDialogButton sm-nativeDialogCancel", onClick: onCancel }, t("bulk.dialog.cancel")),
|
|
1091
|
+
h("button", { type: "button", className: "sm-nativeDialogButton sm-nativeDialogConfirm", onClick: submit }, confirmLabel)
|
|
1092
|
+
)
|
|
1093
|
+
)
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function BulkChoiceDialog(props) {
|
|
1098
|
+
const t = typeof props.t === "function" ? props.t : (key) => key;
|
|
1099
|
+
const open = props.open === true;
|
|
1100
|
+
const [zIndex] = useState(() => open ? nextDialogZ() : 0);
|
|
1101
|
+
const title = props.title || "";
|
|
1102
|
+
const description = props.description || "";
|
|
1103
|
+
const options = Array.isArray(props.options) ? props.options : [];
|
|
1104
|
+
const onCancel = typeof props.onCancel === "function" ? props.onCancel : () => {};
|
|
1105
|
+
const onApply = typeof props.onApply === "function" ? props.onApply : () => {};
|
|
1106
|
+
const confirmLabel = props.confirmLabel || t("bulk.dialog.execute");
|
|
1107
|
+
const [value, setValue] = useState("");
|
|
1108
|
+
useEffect(() => { if (open) setValue(""); }, [open]);
|
|
1109
|
+
const escStateRef = useRef({});
|
|
1110
|
+
escStateRef.current = { onCancel };
|
|
1111
|
+
useEffect(() => {
|
|
1112
|
+
if (!open) return;
|
|
1113
|
+
const handler = (e) => { if (e.key !== "Escape" || e.defaultPrevented) return; e.preventDefault(); e.stopPropagation(); escStateRef.current.onCancel(); };
|
|
1114
|
+
window.addEventListener("keydown", handler);
|
|
1115
|
+
return () => window.removeEventListener("keydown", handler);
|
|
1116
|
+
}, [open]);
|
|
1117
|
+
if (!open) return null;
|
|
1118
|
+
return h("div", { className: "sm-nativeDialogLayer sm-bulkDialog", role: "presentation", style: { zIndex: zIndex || undefined } },
|
|
1119
|
+
h("section", { className: "sm-nativeDialog", role: "dialog", "aria-modal": "true", "aria-labelledby": "sm-bulk-choice-title" },
|
|
1120
|
+
h("div", { className: "sm-nativeDialogHeader" }, h("h2", { id: "sm-bulk-choice-title", className: "sm-nativeDialogTitle" }, title)),
|
|
1121
|
+
h("div", { className: "sm-nativeDialogBody" },
|
|
1122
|
+
description ? h("div", { className: "sm-bulkDialogSummary" }, description) : null,
|
|
1123
|
+
options.length === 0
|
|
1124
|
+
? h("div", { className: "sm-empty" }, t("migrate.noPresets"))
|
|
1125
|
+
: h("select", { className: "sm-viewSelect", value, onChange: (e) => setValue(e.target.value), "aria-label": title, autoFocus: true },
|
|
1126
|
+
h("option", { value: "" }, "—"),
|
|
1127
|
+
options.map((opt) => h("option", { key: opt.value, value: opt.value }, opt.label))
|
|
1128
|
+
)
|
|
1129
|
+
),
|
|
1130
|
+
h("div", { className: "sm-nativeDialogFooter" },
|
|
1131
|
+
h("button", { type: "button", className: "sm-nativeDialogButton sm-nativeDialogCancel", onClick: onCancel }, t("bulk.dialog.cancel")),
|
|
1132
|
+
h("button", { type: "button", className: "sm-nativeDialogButton sm-nativeDialogConfirm", disabled: value === "", onClick: () => onApply(value) }, confirmLabel)
|
|
1133
|
+
)
|
|
1134
|
+
)
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
function AnnotationDialog({ sessionId, displayTitle, t, onClose, anchor }) {
|
|
1138
|
+
const annotations = useAnnotations();
|
|
1139
|
+
const latest = annotationOf(annotations, sessionId);
|
|
1140
|
+
const [base, setBase] = useState(() => latest);
|
|
1141
|
+
const [favorite, setFavorite] = useState(() => latest.favorite);
|
|
1142
|
+
const [reviewLater, setReviewLater] = useState(() => latest.reviewLater);
|
|
1143
|
+
const [tagsText, setTagsText] = useState(() => latest.tags.join(", "));
|
|
1144
|
+
const [note, setNote] = useState(() => latest.note);
|
|
1145
|
+
const [priority, setPriority] = useState(() => String(latest.priority ?? 3));
|
|
1146
|
+
const [busy, setBusy] = useState(false);
|
|
1147
|
+
const [error, setError] = useState(null);
|
|
1148
|
+
const dialogRef = useRef(null);
|
|
1149
|
+
// Claim a fresh z-index so this popup always sits above any earlier
|
|
1150
|
+
// popup the user may have opened (annotation popup typically opens on
|
|
1151
|
+
// top of a confirm/move/migrate dialog, so we need a guaranteed-
|
|
1152
|
+
// higher z-index than those static CSS values).
|
|
1153
|
+
const [zIndex] = useState(() => nextDialogZ());
|
|
1154
|
+
const dirty = favorite !== base.favorite || reviewLater !== base.reviewLater || tagsText !== base.tags.join(", ") || note !== base.note || priority !== String(base.priority ?? 3);
|
|
1155
|
+
const cancel = () => { if (busy || dirty && !window.confirm(t("marks.discard"))) return false; onClose(); return true; };
|
|
1156
|
+
const reload = () => {
|
|
1157
|
+
if (dirty && !window.confirm(t("marks.discard"))) return;
|
|
1158
|
+
setBase(latest); setFavorite(latest.favorite); setReviewLater(latest.reviewLater);
|
|
1159
|
+
setTagsText(latest.tags.join(", ")); setNote(latest.note); setPriority(String(latest.priority ?? 3)); setError(null);
|
|
1160
|
+
};
|
|
1161
|
+
const save = async () => {
|
|
1162
|
+
if (busy || annotations.pending.has(sessionId)) return;
|
|
1163
|
+
setBusy(true); setError(null);
|
|
1164
|
+
try {
|
|
1165
|
+
await annotationStore.save(sessionId, { favorite, reviewLater, tags: tagsText.split(/[,,\n]/).map(tag => tag.trim()).filter(Boolean), note, priority: Number(priority) }, base.revision);
|
|
1166
|
+
onClose();
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
1169
|
+
if (error?.code === "annotation-conflict") await annotationStore.load(true);
|
|
1170
|
+
} finally { setBusy(false); }
|
|
1171
|
+
};
|
|
1172
|
+
const escStateRef = useRef({}); escStateRef.current = { busy, cancel };
|
|
1173
|
+
useEffect(() => {
|
|
1174
|
+
const handler = e => {
|
|
1175
|
+
if (e.key !== "Escape" || e.isComposing || e.keyCode === 229) return;
|
|
1176
|
+
if (e.defaultPrevented) return;
|
|
1177
|
+
if (escStateRef.current.busy) return;
|
|
1178
|
+
e.preventDefault(); e.stopPropagation();
|
|
1179
|
+
if (escStateRef.current.cancel()) {
|
|
1180
|
+
const ae = document.activeElement;
|
|
1181
|
+
if (ae && typeof ae.blur === "function" && ae !== document.body) ae.blur();
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
window.addEventListener("keydown", handler);
|
|
1185
|
+
return () => window.removeEventListener("keydown", handler);
|
|
1186
|
+
}, []);
|
|
1187
|
+
|
|
1188
|
+
const onKeyDown = e => {
|
|
1189
|
+
if (e.key !== "Tab" || !dialogRef.current) return;
|
|
1190
|
+
const controls = [...dialogRef.current.querySelectorAll('button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled)')];
|
|
1191
|
+
if (!controls.length) return;
|
|
1192
|
+
const first = controls[0], last = controls[controls.length - 1];
|
|
1193
|
+
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
|
|
1194
|
+
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
|
|
1195
|
+
};
|
|
1196
|
+
const [importStatus, setImportStatus] = useState(null);
|
|
1197
|
+
const copyPrompt = async () => {
|
|
1198
|
+
if (busy) return;
|
|
1199
|
+
const text = promptFor(activeLanguage);
|
|
1200
|
+
try {
|
|
1201
|
+
if (navigator && navigator.clipboard && typeof navigator.clipboard.writeText === "function") await navigator.clipboard.writeText(text);
|
|
1202
|
+
else { const ta = document.createElement("textarea"); ta.value = text; ta.setAttribute("readonly", ""); ta.style.position = "absolute"; ta.style.left = "-9999px"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); }
|
|
1203
|
+
setImportStatus(t("marks.import.copy.copied"));
|
|
1204
|
+
} catch (err) { setImportStatus(t("marks.import.copy.failed", { message: err instanceof Error ? err.message : String(err) })); }
|
|
1205
|
+
};
|
|
1206
|
+
const [importText, setImportText] = useState("");
|
|
1207
|
+
const pasteFromInput = () => {
|
|
1208
|
+
if (busy) return;
|
|
1209
|
+
const text = (importText || "").trim();
|
|
1210
|
+
if (!text) { setImportStatus(t("marks.import.failure", { message: t("marks.import.empty") })); return; }
|
|
1211
|
+
if (dirty && !window.confirm(t("marks.import.discard"))) { setImportStatus(null); return; }
|
|
1212
|
+
const result = parseImportedAnnotation(text);
|
|
1213
|
+
if (!result.ok) { setImportStatus(t("marks.import.failure", { message: result.error })); return; }
|
|
1214
|
+
const ann = result.annotation;
|
|
1215
|
+
if (Array.isArray(ann.tags)) setTagsText(ann.tags.join(", "));
|
|
1216
|
+
if (typeof ann.note === "string") setNote(ann.note);
|
|
1217
|
+
if ("priority" in ann) setPriority(String(ann.priority ?? 3));
|
|
1218
|
+
setError(null);
|
|
1219
|
+
const warningLines = (result.warnings || []).map(w => {
|
|
1220
|
+
const field = w.field || t("marks.import.unknown");
|
|
1221
|
+
const detail = w.kind === "truncated" && typeof w.from === "number" && typeof w.to === "number"
|
|
1222
|
+
? field + ": " + w.from + " -> " + w.to
|
|
1223
|
+
: w.kind === "rejected" && w.value !== undefined ? field + ": " + JSON.stringify(w.value)
|
|
1224
|
+
: w.kind ? field + " (" + w.kind + ")" : field;
|
|
1225
|
+
return " - " + detail + (w.reason ? " - " + w.reason : "");
|
|
1226
|
+
});
|
|
1227
|
+
if (warningLines.length) setImportStatus(t("marks.import.warnings") + String.fromCharCode(10) + warningLines.join(String.fromCharCode(10)));
|
|
1228
|
+
else setImportStatus(t("marks.import.success"));
|
|
1229
|
+
};
|
|
1230
|
+
// Detect the active UI language by probing a key both dictionaries define.
|
|
1231
|
+
// Falls back to the cached window flag (set elsewhere if available) and ultimately
|
|
1232
|
+
// to "zh" so the prompt never reads empty.
|
|
1233
|
+
const detectLanguage = (translator) => {
|
|
1234
|
+
if (typeof window !== "undefined" && window.__smActiveLanguage) return window.__smActiveLanguage;
|
|
1235
|
+
const sample = translator("marks.favorite");
|
|
1236
|
+
if (typeof sample === "string") {
|
|
1237
|
+
if (sample === "Favorite") return "en";
|
|
1238
|
+
if (sample === "收藏") return "zh";
|
|
1239
|
+
}
|
|
1240
|
+
return "zh";
|
|
1241
|
+
};
|
|
1242
|
+
const activeLanguage = detectLanguage(t);
|
|
1243
|
+
const hasAnchor = anchor && typeof anchor.top === "number" && typeof anchor.left === "number";
|
|
1244
|
+
const layerClass = "sm-nativeDialogLayer sm-annotationDialog sm-confirmDialog" + (hasAnchor ? " sm-annotationDialog-anchored" : "");
|
|
1245
|
+
const surfaceClass = "sm-nativeDialog sm-panelDialog sm-annotationSurface" + (hasAnchor ? " sm-annotationSurface-anchored" : "");
|
|
1246
|
+
// Default position matches ConfirmDialog / MoveDialog so all inner
|
|
1247
|
+
// dialogs line up at top calc(50vh - 90px), left calc(50vw + 308px).
|
|
1248
|
+
const layerStyle = {
|
|
1249
|
+
zIndex: zIndex || undefined,
|
|
1250
|
+
...(hasAnchor ? { position: "fixed", top: anchor.top + "px", left: anchor.left + "px" } : {})
|
|
1251
|
+
};
|
|
1252
|
+
return h("div", { className: layerClass, style: layerStyle, role: "presentation" },
|
|
1253
|
+
h("section", { className: surfaceClass, role: "dialog", "aria-labelledby": "sm-annotation-title", ref: dialogRef, onKeyDown },
|
|
1254
|
+
h("div", { className: "sm-nativeDialogHeader" }, h("h2", { id: "sm-annotation-title", className: "sm-nativeDialogTitle" }, t("marks.title"))),
|
|
1255
|
+
h("div", { className: "sm-annotationBody" },
|
|
1256
|
+
h("div", { className: "sm-annotationTarget", title: displayTitle || sessionId }, displayTitle || sessionId),
|
|
1257
|
+
h("div", { className: "sm-annotationChecks" },
|
|
1258
|
+
h("label", null, h("input", { type: "checkbox", "aria-label": t("marks.favorite"), checked: favorite, disabled: busy, onChange: e => setFavorite(e.target.checked) }), t("marks.favorite")),
|
|
1259
|
+
h("label", { className: "sm-annotationPriority" },
|
|
1260
|
+
h("span", null, t("marks.priority")),
|
|
1261
|
+
h("select", { className: "sm-viewSelect sm-prioritySelectInline", "aria-label": t("marks.priority"), value: priority, disabled: busy, onChange: e => setPriority(e.target.value) },
|
|
1262
|
+
[1,2,3,4,5].map(value => h("option", { key: value, value: String(value) }, t("marks.priority." + value)))
|
|
1263
|
+
)
|
|
1264
|
+
),
|
|
1265
|
+
h("label", null, h("input", { type: "checkbox", "aria-label": t("marks.review"), checked: reviewLater, disabled: busy, onChange: e => setReviewLater(e.target.checked) }), t("marks.review")),
|
|
1266
|
+
h("small", null, t("marks.priority.help"))
|
|
1267
|
+
),
|
|
1268
|
+
h("label", { className: "sm-viewField sm-fieldWithClear" }, h("span", null, t("marks.tags")), h("div", { className: "sm-fieldRow sm-fieldRowNote" }, h("textarea", { className: "sm-noteInput", "aria-label": t("marks.tags"), value: tagsText, maxLength: 1000, autoFocus: true, rows: 3, placeholder: t("marks.tags.placeholder"), disabled: busy, onChange: e => setTagsText(e.target.value) }), tagsText && !busy ? h("button", { type: "button", className: "sm-fieldClear sm-fieldClearNote", "aria-label": t("marks.tags.clear"), title: t("marks.tags.clear"), onClick: () => setTagsText("") }, String.fromCharCode(10005)) : null)),
|
|
1269
|
+
h("label", { className: "sm-viewField sm-fieldWithClear" }, h("span", null, t("marks.note")), h("div", { className: "sm-fieldRow sm-fieldRowNote" }, h("textarea", { className: "sm-noteInput", "aria-label": t("marks.note"), value: note, maxLength: 2000, rows: 3, placeholder: t("marks.note.placeholder"), disabled: busy, onChange: e => setNote(e.target.value) }), note && !busy ? h("button", { type: "button", className: "sm-fieldClear sm-fieldClearNote", "aria-label": t("marks.note.clear"), title: t("marks.note.clear"), onClick: () => setNote("") }, String.fromCharCode(10005)) : null)),
|
|
1270
|
+
latest.revision !== base.revision ? h("div", { className: "sm-markConflict", role: "status" }, t("marks.conflict"), h("button", { type: "button", className: "sm-resetFilters", disabled: busy, onClick: reload }, t("marks.reload"))) : null,
|
|
1271
|
+
annotations.error ? h("div", { className: "sm-error", role: "alert" }, t("marks.error", { message: annotations.error }), h("button", { type: "button", className: "sm-resetFilters", disabled: busy || annotations.loading, onClick: () => annotationStore.load(true) }, t("marks.retry"))) : null,
|
|
1272
|
+
error ? h("div", { className: "sm-error", role: "alert" }, error) : null
|
|
1273
|
+
),
|
|
1274
|
+
h("div", { className: "sm-importGroup" },
|
|
1275
|
+
h("div", { className: "sm-importGroupButtons" },
|
|
1276
|
+
h("button", { type: "button", className: "sm-importBtn", disabled: busy || !importText.trim(), onClick: pasteFromInput }, t("marks.import.paste")),
|
|
1277
|
+
h("button", { type: "button", className: "sm-importBtn", disabled: busy, onClick: copyPrompt }, t("marks.import.copy"))),
|
|
1278
|
+
h("div", { className: "sm-fieldWithClear" },
|
|
1279
|
+
h("div", { className: "sm-fieldRow sm-fieldRowNote" },
|
|
1280
|
+
h("textarea", { className: "sm-noteInput", "aria-label": t("marks.import.paste.aria"), value: importText, placeholder: t("marks.import.paste.placeholder"), disabled: busy, rows: 3, onChange: e => setImportText(e.target.value) }),
|
|
1281
|
+
importText && !busy ? h("button", { type: "button", className: "sm-fieldClear sm-fieldClearNote", "aria-label": t("marks.import.paste.clear"), title: t("marks.import.paste.clear"), onClick: () => setImportText("") }, String.fromCharCode(10005)) : null))),
|
|
1282
|
+
importStatus ? h("div", { className: "sm-importStatus", role: "status" }, importStatus) : null,
|
|
1283
|
+
h("div", { className: "sm-nativeDialogFooter" },
|
|
1284
|
+
h("button", { type: "button", className: "sm-nativeDialogButton", disabled: busy, onClick: cancel }, t("marks.cancel")),
|
|
1285
|
+
h("button", { type: "button", className: "sm-nativeDialogButton sm-nativeDialogConfirm", disabled: busy || !annotations.loaded || !!annotations.error || latest.revision !== base.revision, onClick: save }, t(busy ? "marks.saving" : "marks.save"))
|
|
1286
|
+
)
|
|
1287
|
+
)
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
|
|
1292
|
+
const formatRelative = (ts, now) => {
|
|
1293
|
+
const timestamp = sessionTime(ts);
|
|
1294
|
+
if (timestamp === null) return "—";
|
|
1295
|
+
const diff = Math.max(0, now - timestamp);
|
|
1296
|
+
const s = Math.floor(diff / 1000);
|
|
1297
|
+
if (s < 60) return s + "s";
|
|
1298
|
+
const m = Math.floor(s / 60);
|
|
1299
|
+
if (m < 60) return m + "m";
|
|
1300
|
+
const hr = Math.floor(m / 60);
|
|
1301
|
+
if (hr < 24) return hr + "h";
|
|
1302
|
+
const d = Math.floor(hr / 24);
|
|
1303
|
+
if (d < 30) return d + "d";
|
|
1304
|
+
const mo = Math.floor(d / 30);
|
|
1305
|
+
if (mo < 12) return mo + "mo";
|
|
1306
|
+
return Math.floor(mo / 12) + "y";
|
|
1307
|
+
};
|
|
1308
|
+
const shortPath = (cwd) => {
|
|
1309
|
+
if (!cwd) return "";
|
|
1310
|
+
const parts = cwd.replace(/[\\\/]+$/, "").split(/[\\\/]/);
|
|
1311
|
+
const last = parts[parts.length - 1] || "";
|
|
1312
|
+
return last.length > 26 ? last.slice(0, 24) + "\u2026" : last;
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1315
|
+
function ConfirmDialog(props) {
|
|
1316
|
+
const open = props.open === true;
|
|
1317
|
+
const onCancel = typeof props.onCancel === "function" ? props.onCancel : () => {};
|
|
1318
|
+
const onConfirm = typeof props.onConfirm === "function" ? props.onConfirm : () => {};
|
|
1319
|
+
const busy = props.busy === true;
|
|
1320
|
+
const danger = props.danger === true;
|
|
1321
|
+
// Claim a fresh z-index so this popup sits above any earlier popup.
|
|
1322
|
+
const [zIndex] = useState(() => open ? nextDialogZ() : 0);
|
|
1323
|
+
const title = props.title || "";
|
|
1324
|
+
const description = props.description || "";
|
|
1325
|
+
const warning = props.warning || "";
|
|
1326
|
+
const confirmLabel = props.confirmLabel || "确认";
|
|
1327
|
+
const cancelLabel = props.cancelLabel || "取消";
|
|
1328
|
+
// Esc is handled at window level (issue #7): the layer-bound
|
|
1329
|
+
// onKeyDown was unreachable when the row button that opened
|
|
1330
|
+
// the dialog stayed focused. ConfirmDialog's portal to body
|
|
1331
|
+
// makes it a separate subtree from the panel section, so
|
|
1332
|
+
// keydown never bubbles through it. Enter was previously on
|
|
1333
|
+
// the layer too, but that races with the focused Cancel /
|
|
1334
|
+
// Confirm button's native Enter handler -- removing the layer
|
|
1335
|
+
// Enter leaves Enter handling purely to the focused button.
|
|
1336
|
+
const escStateRef = useRef({});
|
|
1337
|
+
escStateRef.current = { busy, onCancel };
|
|
1338
|
+
useEffect(() => {
|
|
1339
|
+
if (!open) return;
|
|
1340
|
+
const handler = (e) => {
|
|
1341
|
+
if (e.key !== "Escape") return;
|
|
1342
|
+
if (e.defaultPrevented) return;
|
|
1343
|
+
const s = escStateRef.current;
|
|
1344
|
+
if (s.busy) return;
|
|
1345
|
+
e.preventDefault();
|
|
1346
|
+
e.stopPropagation();
|
|
1347
|
+
s.onCancel();
|
|
1348
|
+
// See SessionManagerPanel for the rationale.
|
|
1349
|
+
const ae = document.activeElement;
|
|
1350
|
+
if (ae && typeof ae.blur === "function" && ae !== document.body) ae.blur();
|
|
1351
|
+
};
|
|
1352
|
+
window.addEventListener("keydown", handler);
|
|
1353
|
+
return () => window.removeEventListener("keydown", handler);
|
|
1354
|
+
}, [open]);
|
|
1355
|
+
// Render inline as a sibling of the panel; createPortal to document.body broke under some hosts. Migrate and AnnotationDialog render inline; do the same here.
|
|
1356
|
+
|
|
1357
|
+
|
|
1358
|
+
if (!open) return null;
|
|
1359
|
+
|
|
1360
|
+
return h("div", {
|
|
1361
|
+
className: "sm-nativeDialogLayer sm-confirmDialog",
|
|
1362
|
+
style: {
|
|
1363
|
+
zIndex: zIndex || undefined,
|
|
1364
|
+
...(props.anchor && typeof props.anchor.top === "number"
|
|
1365
|
+
? { position: "fixed", top: props.anchor.top + "px", left: props.anchor.left + "px" }
|
|
1366
|
+
: {})
|
|
1367
|
+
},
|
|
1368
|
+
role: "presentation"
|
|
1369
|
+
},
|
|
1370
|
+
|
|
1371
|
+
h("section", {
|
|
1372
|
+
className: "sm-nativeDialog",
|
|
1373
|
+
role: "dialog",
|
|
1374
|
+
"aria-modal": "true",
|
|
1375
|
+
"aria-labelledby": "sm-confirm-dialog-title"
|
|
1376
|
+
},
|
|
1377
|
+
h("div", { className: "sm-nativeDialogHeader" },
|
|
1378
|
+
h("h2", { id: "sm-confirm-dialog-title", className: "sm-nativeDialogTitle" }, title)
|
|
1379
|
+
),
|
|
1380
|
+
h("div", { className: "sm-nativeDialogBody" },
|
|
1381
|
+
description ? h("div", { className: "sm-nativeDialogDescription" }, description) : null,
|
|
1382
|
+
warning ? h("div", { className: "sm-warn" }, h("span", null, warning)) : null
|
|
1383
|
+
),
|
|
1384
|
+
h("div", { className: "sm-nativeDialogFooter" },
|
|
1385
|
+
h("button", {
|
|
1386
|
+
type: "button",
|
|
1387
|
+
className: "sm-nativeDialogButton sm-nativeDialogCancel",
|
|
1388
|
+
disabled: busy,
|
|
1389
|
+
onClick: () => { if (!busy) onCancel(); }
|
|
1390
|
+
}, cancelLabel),
|
|
1391
|
+
h("button", {
|
|
1392
|
+
type: "button",
|
|
1393
|
+
className: "sm-nativeDialogButton" + (danger ? " sm-nativeDialogDanger" : " sm-nativeDialogConfirm"),
|
|
1394
|
+
disabled: busy,
|
|
1395
|
+
onClick: () => { if (!busy) onConfirm(); }
|
|
1396
|
+
}, confirmLabel)
|
|
1397
|
+
)
|
|
1398
|
+
)
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function MoveDialog(props) {
|
|
1403
|
+
const t = typeof props.t === "function" ? props.t : (key) => key;
|
|
1404
|
+
const open = props.open === true;
|
|
1405
|
+
const onCancel = typeof props.onCancel === "function" ? props.onCancel : () => {};
|
|
1406
|
+
const onConfirm = typeof props.onConfirm === "function" ? props.onConfirm : () => {};
|
|
1407
|
+
const busy = props.busy === true;
|
|
1408
|
+
const title = props.title || t("confirm.move.title");
|
|
1409
|
+
const description = props.description || "";
|
|
1410
|
+
const currentWorkspaceId = props.currentWorkspaceId || "";
|
|
1411
|
+
const confirmLabel = props.confirmLabel || t("confirm.move.confirm");
|
|
1412
|
+
const cancelLabel = props.cancelLabel || t("confirm.cancel");
|
|
1413
|
+
const error = props.error || "";
|
|
1414
|
+
// Claim a fresh z-index so this popup sits above any earlier popup.
|
|
1415
|
+
const [zIndex] = useState(() => open ? nextDialogZ() : 0);
|
|
1416
|
+
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState("");
|
|
1417
|
+
// Esc handled at window level (issue #7) -- Enter stays on the
|
|
1418
|
+
// layer because (a) it gates on selectedWorkspaceId which is set
|
|
1419
|
+
// only by clicking a listbox item, and (b) racing Enter on the
|
|
1420
|
+
// Cancel / Confirm buttons is benign since the layer Enter
|
|
1421
|
+
// handler only fires when focus is inside the dialog subtree,
|
|
1422
|
+
// which is exactly when the focused button's native Enter has
|
|
1423
|
+
// already triggered its own click -- and in MoveDialog that
|
|
1424
|
+
// click just toggles selection, never cancels or confirms.
|
|
1425
|
+
const escStateRef = useRef({});
|
|
1426
|
+
escStateRef.current = { busy, onCancel };
|
|
1427
|
+
useEffect(() => {
|
|
1428
|
+
if (!open) return;
|
|
1429
|
+
const handler = (e) => {
|
|
1430
|
+
if (e.key !== "Escape") return;
|
|
1431
|
+
if (e.defaultPrevented) return;
|
|
1432
|
+
const s = escStateRef.current;
|
|
1433
|
+
if (s.busy) return;
|
|
1434
|
+
e.preventDefault();
|
|
1435
|
+
e.stopPropagation();
|
|
1436
|
+
s.onCancel();
|
|
1437
|
+
// See SessionManagerPanel for the rationale.
|
|
1438
|
+
const ae = document.activeElement;
|
|
1439
|
+
if (ae && typeof ae.blur === "function" && ae !== document.body) ae.blur();
|
|
1440
|
+
};
|
|
1441
|
+
window.addEventListener("keydown", handler);
|
|
1442
|
+
return () => window.removeEventListener("keydown", handler);
|
|
1443
|
+
}, [open]);
|
|
1444
|
+
if (!open) return null;
|
|
1445
|
+
|
|
1446
|
+
const list = (Array.isArray(props.workspaces) ? props.workspaces : [])
|
|
1447
|
+
.filter((ws) => ws && typeof ws === "object" && typeof ws.id === "string" && ws.id !== currentWorkspaceId);
|
|
1448
|
+
const body = h("div", { className: "sm-nativeDialogBody" },
|
|
1449
|
+
h("div", { className: "sm-nativeDialogDescription" }, description),
|
|
1450
|
+
list.length === 0
|
|
1451
|
+
? h("div", { className: "sm-moveLabel" }, t("confirm.move.empty"))
|
|
1452
|
+
: h("div", { className: "sm-moveSection" },
|
|
1453
|
+
h("div", { className: "sm-moveLabel" }, t("confirm.move.select")),
|
|
1454
|
+
h("div", { className: "sm-moveList", role: "listbox", "aria-label": t("confirm.move.select") },
|
|
1455
|
+
list.map((ws) => h("button", {
|
|
1456
|
+
key: ws.id,
|
|
1457
|
+
type: "button",
|
|
1458
|
+
role: "option",
|
|
1459
|
+
"aria-selected": selectedWorkspaceId === ws.id,
|
|
1460
|
+
className: "sm-moveItem" + (selectedWorkspaceId === ws.id ? " sm-moveItemSelected" : ""),
|
|
1461
|
+
disabled: busy,
|
|
1462
|
+
onClick: () => setSelectedWorkspaceId(ws.id)
|
|
1463
|
+
},
|
|
1464
|
+
h("span", { className: "sm-moveItemName" }, String(ws.title || ws.name || ws.id)),
|
|
1465
|
+
h("span", { className: "sm-moveItemMeta" }, ws.path ? String(ws.path) : "")
|
|
1466
|
+
))
|
|
1467
|
+
)
|
|
1468
|
+
),
|
|
1469
|
+
error ? h("div", { className: "sm-error", role: "alert" }, String(error)) : null
|
|
1470
|
+
);
|
|
1471
|
+
// Single cancel button (footer) + ESC handler. No "x" close button so the
|
|
1472
|
+
// user only sees one cancel entry point.
|
|
1473
|
+
const onKeyDown = (e) => {
|
|
1474
|
+
// Enter only -- Esc moved to a window-level listener (see above).
|
|
1475
|
+
if (e.key === "Enter" && !busy && selectedWorkspaceId && list.length > 0) {
|
|
1476
|
+
e.preventDefault();
|
|
1477
|
+
onConfirm(selectedWorkspaceId);
|
|
1478
|
+
}
|
|
1479
|
+
};
|
|
1480
|
+
return h("div", {
|
|
1481
|
+
className: "sm-nativeDialogLayer sm-confirmDialog",
|
|
1482
|
+
style: {
|
|
1483
|
+
zIndex: zIndex || undefined,
|
|
1484
|
+
...(props.anchor && typeof props.anchor.top === "number"
|
|
1485
|
+
? { position: "fixed", top: props.anchor.top + "px", left: props.anchor.left + "px" }
|
|
1486
|
+
: {})
|
|
1487
|
+
},
|
|
1488
|
+
role: "presentation",
|
|
1489
|
+
onKeyDown
|
|
1490
|
+
},
|
|
1491
|
+
|
|
1492
|
+
h("section", {
|
|
1493
|
+
className: "sm-nativeDialog",
|
|
1494
|
+
role: "dialog",
|
|
1495
|
+
"aria-modal": "true",
|
|
1496
|
+
"aria-labelledby": "sm-move-dialog-title"
|
|
1497
|
+
},
|
|
1498
|
+
h("div", { className: "sm-nativeDialogHeader" },
|
|
1499
|
+
h("h2", { id: "sm-move-dialog-title", className: "sm-nativeDialogTitle" }, title)
|
|
1500
|
+
),
|
|
1501
|
+
body,
|
|
1502
|
+
h("div", { className: "sm-nativeDialogFooter" },
|
|
1503
|
+
h("button", {
|
|
1504
|
+
type: "button",
|
|
1505
|
+
className: "sm-nativeDialogButton sm-nativeDialogCancel",
|
|
1506
|
+
disabled: busy,
|
|
1507
|
+
onClick: onCancel
|
|
1508
|
+
}, cancelLabel),
|
|
1509
|
+
h("button", {
|
|
1510
|
+
type: "button",
|
|
1511
|
+
className: "sm-nativeDialogButton sm-nativeDialogConfirm",
|
|
1512
|
+
disabled: busy || !selectedWorkspaceId || list.length === 0,
|
|
1513
|
+
onClick: () => { if (!busy && selectedWorkspaceId) onConfirm(selectedWorkspaceId); }
|
|
1514
|
+
}, confirmLabel)
|
|
1515
|
+
)
|
|
1516
|
+
)
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
function MigratePresetDialog(props) {
|
|
1521
|
+
const t = typeof props.t === "function" ? props.t : (key) => key;
|
|
1522
|
+
const open = props.open === true;
|
|
1523
|
+
const onCancel = typeof props.onCancel === "function" ? props.onCancel : () => {};
|
|
1524
|
+
const onRun = typeof props.onRun === "function" ? props.onRun : () => {};
|
|
1525
|
+
const busy = props.busy === true;
|
|
1526
|
+
const sessionId = props.sessionId || "";
|
|
1527
|
+
const fallbackPreset = props.currentPreset || "";
|
|
1528
|
+
const [scan, setScan] = useState(null);
|
|
1529
|
+
const [target, setTarget] = useState("");
|
|
1530
|
+
// Claim a fresh z-index so this popup sits above any earlier popup.
|
|
1531
|
+
const [zIndex] = useState(() => open ? nextDialogZ() : 0);
|
|
1532
|
+
useEffect(() => {
|
|
1533
|
+
if (!open || !sessionId) return;
|
|
1534
|
+
let alive = true;
|
|
1535
|
+
// Skip setScan(null)/setTarget("") -- no-ops on initial mount that
|
|
1536
|
+
// would force an extra render cycle, slowing dialog open.
|
|
1537
|
+
fetchApi("/preset-scan?sessionId=" + encodeURIComponent(sessionId)).then((d) => {
|
|
1538
|
+
if (!alive) return;
|
|
1539
|
+
if (d && d.ok) {
|
|
1540
|
+
const result = d.result || {};
|
|
1541
|
+
const rows = Array.isArray(result.rows)
|
|
1542
|
+
? result.rows
|
|
1543
|
+
: (result.rows && Array.isArray(result.rows.rows) ? result.rows.rows : []);
|
|
1544
|
+
const availablePresets = Array.isArray(result.availablePresets)
|
|
1545
|
+
? result.availablePresets
|
|
1546
|
+
: (result.rows && Array.isArray(result.rows.availablePresets) ? result.rows.availablePresets : []);
|
|
1547
|
+
const row = rows.find((item) => item && item.sessionId === sessionId) || null;
|
|
1548
|
+
setScan({ ok: true, row, availablePresets });
|
|
1549
|
+
} else {
|
|
1550
|
+
setScan({ ok: false, error: (d && d.error) || "scan failed" });
|
|
1551
|
+
}
|
|
1552
|
+
}).catch((error) => {
|
|
1553
|
+
if (alive) setScan({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
1554
|
+
});
|
|
1555
|
+
return () => { alive = false; };
|
|
1556
|
+
}, [open, sessionId]);
|
|
1557
|
+
// Esc handled at window level (issue #7). MigratePresetDialog is
|
|
1558
|
+
// a sibling of the panel layer in the panel's render tree, so
|
|
1559
|
+
// the layer-bound onKeyDown (which the dialog relied on for
|
|
1560
|
+
// Esc) was dead code -- focus never reached the dialog's own
|
|
1561
|
+
// subtree because the row Migrate button stayed focused.
|
|
1562
|
+
const escStateRef = useRef({});
|
|
1563
|
+
escStateRef.current = { busy, onCancel };
|
|
1564
|
+
useEffect(() => {
|
|
1565
|
+
if (!open) return;
|
|
1566
|
+
const handler = (e) => {
|
|
1567
|
+
if (e.key !== "Escape") return;
|
|
1568
|
+
if (e.defaultPrevented) return;
|
|
1569
|
+
const s = escStateRef.current;
|
|
1570
|
+
if (s.busy) return;
|
|
1571
|
+
e.preventDefault();
|
|
1572
|
+
e.stopPropagation();
|
|
1573
|
+
s.onCancel();
|
|
1574
|
+
// See SessionManagerPanel for the rationale.
|
|
1575
|
+
const ae = document.activeElement;
|
|
1576
|
+
if (ae && typeof ae.blur === "function" && ae !== document.body) ae.blur();
|
|
1577
|
+
};
|
|
1578
|
+
window.addEventListener("keydown", handler);
|
|
1579
|
+
return () => window.removeEventListener("keydown", handler);
|
|
1580
|
+
}, [open]);
|
|
1581
|
+
if (!open) return null;
|
|
1582
|
+
|
|
1583
|
+
const currentPreset = (scan && scan.ok && scan.row && (scan.row.finalPreset || scan.row.headerPreset)) || fallbackPreset || "—";
|
|
1584
|
+
const presets = scan && scan.ok
|
|
1585
|
+
? Array.from(new Set((scan.availablePresets || []).filter((preset) => typeof preset === "string" && preset.length > 0))).sort()
|
|
1586
|
+
: [];
|
|
1587
|
+
const targets = presets.filter((preset) => preset !== currentPreset);
|
|
1588
|
+
const ready = target !== "" && target !== currentPreset;
|
|
1589
|
+
const body = h("div", { className: "sm-migratePanel" },
|
|
1590
|
+
scan === null
|
|
1591
|
+
? h("div", { className: "sm-empty" }, t("busy.processing"))
|
|
1592
|
+
: !scan.ok
|
|
1593
|
+
? h("div", { className: "sm-error", role: "alert" }, scan.error || "scan failed")
|
|
1594
|
+
: h(React.Fragment, null,
|
|
1595
|
+
h("div", { className: "sm-migrateRow" },
|
|
1596
|
+
h("label", null, t("migrate.current")),
|
|
1597
|
+
h("div", { className: "sm-migrateCurrent" }, currentPreset)
|
|
1598
|
+
),
|
|
1599
|
+
h("div", { className: "sm-migrateRow" },
|
|
1600
|
+
h("label", null, t("migrate.target")),
|
|
1601
|
+
targets.length === 0
|
|
1602
|
+
? h("div", { className: "sm-migrateEmpty" }, t("migrate.noPresets"))
|
|
1603
|
+
: h("div", { className: "sm-migrateChips" }, targets.map((preset) => h("button", {
|
|
1604
|
+
key: preset,
|
|
1605
|
+
type: "button",
|
|
1606
|
+
className: "sm-migrateChip" + (target === preset ? " sm-migrateChipOn" : ""),
|
|
1607
|
+
disabled: busy,
|
|
1608
|
+
onClick: () => setTarget(preset)
|
|
1609
|
+
}, preset)))
|
|
1610
|
+
),
|
|
1611
|
+
target === currentPreset ? h("div", { className: "sm-migrateWarn" }, t("migrate.same")) : null
|
|
1612
|
+
)
|
|
1613
|
+
);
|
|
1614
|
+
return h("div", {
|
|
1615
|
+
className: "sm-nativeDialogLayer sm-confirmDialog sm-migrateConfirmLayer",
|
|
1616
|
+
role: "presentation",
|
|
1617
|
+
style: { zIndex: zIndex || undefined }
|
|
1618
|
+
},
|
|
1619
|
+
|
|
1620
|
+
h("section", {
|
|
1621
|
+
className: "sm-nativeDialog sm-migrateDialog",
|
|
1622
|
+
role: "dialog",
|
|
1623
|
+
"aria-modal": "true",
|
|
1624
|
+
"aria-labelledby": "sm-migrate-dialog-title"
|
|
1625
|
+
},
|
|
1626
|
+
h("div", { className: "sm-nativeDialogHeader" },
|
|
1627
|
+
h("h2", { id: "sm-migrate-dialog-title", className: "sm-nativeDialogTitle" }, t("migrate.title"))
|
|
1628
|
+
),
|
|
1629
|
+
body,
|
|
1630
|
+
h("div", { className: "sm-nativeDialogFooter" },
|
|
1631
|
+
h("button", {
|
|
1632
|
+
type: "button",
|
|
1633
|
+
className: "sm-nativeDialogButton sm-nativeDialogCancel",
|
|
1634
|
+
disabled: busy,
|
|
1635
|
+
onClick: onCancel
|
|
1636
|
+
}, t("confirm.cancel")),
|
|
1637
|
+
h("button", {
|
|
1638
|
+
type: "button",
|
|
1639
|
+
className: "sm-nativeDialogButton sm-nativeDialogConfirm",
|
|
1640
|
+
disabled: busy || !ready,
|
|
1641
|
+
onClick: () => { if (ready) onRun(target); }
|
|
1642
|
+
}, busy ? t("busy.processing") : t("migrate.confirm"))
|
|
1643
|
+
)
|
|
1644
|
+
)
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
const panelStore = {
|
|
1649
|
+
open: false,
|
|
1650
|
+
listeners: new Set(),
|
|
1651
|
+
getSnapshot() { return this.open; },
|
|
1652
|
+
subscribe(cb) { this.listeners.add(cb); return () => this.listeners.delete(cb); },
|
|
1653
|
+
set(v) { if (this.open === v) return; this.open = v; for (const fn of [...this.listeners]) fn(); },
|
|
1654
|
+
toggle() { this.open = !this.open; for (const fn of [...this.listeners]) fn(); },
|
|
1655
|
+
// Pending action queued by the title-bar buttons (e.g. 移动至工作区).
|
|
1656
|
+
// The SessionManagerPanel consumes this on mount so it can pre-open
|
|
1657
|
+
// the matching inner dialog (move / delete / migrate) next to itself.
|
|
1658
|
+
pendingMove: null,
|
|
1659
|
+
pendingListeners: new Set(),
|
|
1660
|
+
_emitPending() { for (const fn of [...this.pendingListeners]) fn(); },
|
|
1661
|
+
subscribePending(cb) { this.pendingListeners.add(cb); return () => this.pendingListeners.delete(cb); },
|
|
1662
|
+
setPendingMove(v) { this.pendingMove = v; this._emitPending(); },
|
|
1663
|
+
consumePendingMove() { const v = this.pendingMove; this.pendingMove = null; return v; }
|
|
1664
|
+
};
|
|
1665
|
+
|
|
1666
|
+
const DEFAULT_SORT = "updated-desc";
|
|
1667
|
+
const SORT_ORDERS = ["updated-desc", "updated-asc", "created-desc", "created-asc", "priority"];
|
|
1668
|
+
const workspaceValue = (id) => "workspace:" + id;
|
|
1669
|
+
const pathKey = (value) => {
|
|
1670
|
+
if (typeof value !== "string" || value === "") return "";
|
|
1671
|
+
const path = value.replace(/\\/g, "/").replace(/\/+$/, "") || "/";
|
|
1672
|
+
return /^[a-z]:(?:\/|$)/i.test(path) || path.startsWith("//") ? path.toLowerCase() : path;
|
|
1673
|
+
};
|
|
1674
|
+
const sessionTime = (value) => {
|
|
1675
|
+
if (typeof value !== "number" && typeof value !== "string") return null;
|
|
1676
|
+
if (typeof value === "string" && value.trim() === "") return null;
|
|
1677
|
+
const time = typeof value === "number" || /^\d+(?:\.\d+)?$/.test(value) ? Number(value) : Date.parse(value);
|
|
1678
|
+
return Number.isFinite(time) ? time : null;
|
|
1679
|
+
};
|
|
1680
|
+
const indexWorkspaces = (workspaces) => {
|
|
1681
|
+
const bySession = new Map();
|
|
1682
|
+
const byPath = new Map();
|
|
1683
|
+
const ids = new Set();
|
|
1684
|
+
for (const workspace of workspaces) {
|
|
1685
|
+
ids.add(workspace.id);
|
|
1686
|
+
const key = pathKey(workspace.path);
|
|
1687
|
+
if (key) {
|
|
1688
|
+
if (!byPath.has(key)) byPath.set(key, new Set());
|
|
1689
|
+
byPath.get(key).add(workspace.id);
|
|
1690
|
+
}
|
|
1691
|
+
for (const id of Array.isArray(workspace.sessionIds) ? workspace.sessionIds : []) {
|
|
1692
|
+
if (!bySession.has(id)) bySession.set(id, new Set());
|
|
1693
|
+
bySession.get(id).add(workspace.id);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
return { bySession, byPath, ids };
|
|
1697
|
+
};
|
|
1698
|
+
const rowWorkspaces = (session, index) => {
|
|
1699
|
+
// Registry membership includes archived sessions and takes precedence
|
|
1700
|
+
// over a stale cwd. Older metadata can fall back to workspaceId/cwd.
|
|
1701
|
+
if (index.bySession.has(session.id)) return index.bySession.get(session.id);
|
|
1702
|
+
if (index.ids.has(session.workspaceId)) return new Set([session.workspaceId]);
|
|
1703
|
+
return index.byPath.get(pathKey(session.cwd)) ?? new Set();
|
|
1704
|
+
};
|
|
1705
|
+
|
|
1706
|
+
function SessionManagerPanel(props) {
|
|
1707
|
+
const t = props.t;
|
|
1708
|
+
const open = props.open;
|
|
1709
|
+
const onClose = props.onClose;
|
|
1710
|
+
const useSessions = props.useSessions;
|
|
1711
|
+
const useWorkspaces = props.useWorkspaces;
|
|
1712
|
+
const onOpen = props.onOpen;
|
|
1713
|
+
const onArchive = props.onArchive;
|
|
1714
|
+
const onUnarchive = props.onUnarchive;
|
|
1715
|
+
const onRemove = props.onRemove;
|
|
1716
|
+
const onMove = props.onMove;
|
|
1717
|
+
const onMigrate = props.onMigrate;
|
|
1718
|
+
const annotations = useAnnotations(true);
|
|
1719
|
+
const marksReady = annotations.loaded && !annotations.error;
|
|
1720
|
+
const [onlyFavorite, setOnlyFavorite] = useState(false);
|
|
1721
|
+
const [onlyReview, setOnlyReview] = useState(false);
|
|
1722
|
+
const [tagFilter, setTagFilter] = useState("all");
|
|
1723
|
+
const [priorityFilter, setPriorityFilter] = useState("all");
|
|
1724
|
+
const [annotationFor, setAnnotationFor] = useState(null);
|
|
1725
|
+
const [annotationAnchor, setAnnotationAnchor] = useState(null);
|
|
1726
|
+
const list = useSessions((s) => s);
|
|
1727
|
+
const workspaceSnapshot = useWorkspaces((s) => s);
|
|
1728
|
+
const archivedIds = workspaceSnapshot.archivedSessionIds || [];
|
|
1729
|
+
const [filter, setFilter] = useState("all");
|
|
1730
|
+
const [query, setQuery] = useState("");
|
|
1731
|
+
const [workspaceFilter, setWorkspaceFilter] = useState("all");
|
|
1732
|
+
const [sortOrder, setSortOrder] = useState(DEFAULT_SORT);
|
|
1733
|
+
const [busyId, setBusyId] = useState(null);
|
|
1734
|
+
const [error, setError] = useState(null);
|
|
1735
|
+
const [now, setNow] = useState(() => Date.now());
|
|
1736
|
+
const [moveFor, setMoveFor] = useState(null);
|
|
1737
|
+
const [confirmFor, setConfirmFor] = useState(null);
|
|
1738
|
+
const [workspaces, setWorkspaces] = useState([]);
|
|
1739
|
+
const [createdTimes, setCreatedTimes] = useState({});
|
|
1740
|
+
const [workspacesLoading, setWorkspacesLoading] = useState(true);
|
|
1741
|
+
const [workspacesError, setWorkspacesError] = useState(null);
|
|
1742
|
+
const [workspaceRefresh, setWorkspaceRefresh] = useState(0);
|
|
1743
|
+
const [migrating, setMigrating] = useState(false);
|
|
1744
|
+
const [migrateFor, setMigrateFor] = useState(null);
|
|
1745
|
+
const sessionMembershipKey = useMemo(() => [...list.ids].sort().join("\0"), [list.ids]);
|
|
1746
|
+
// ----- Bulk-action selection state -----
|
|
1747
|
+
const [selectedIds, setSelectedIds] = useState(() => new Set());
|
|
1748
|
+
const [lastClickedRank, setLastClickedRank] = useState(null);
|
|
1749
|
+
const [bulkPreview, setBulkPreview] = useState(null);
|
|
1750
|
+
const [bulkProgress, setBulkProgress] = useState(null);
|
|
1751
|
+
const [bulkResult, setBulkResult] = useState(null);
|
|
1752
|
+
const [bulkBusy, setBulkBusy] = useState(false);
|
|
1753
|
+
const [bulkTagDraft, setBulkTagDraft] = useState(null);
|
|
1754
|
+
// Select-mode is the user-controlled switch that reveals the row
|
|
1755
|
+
// checkboxes and the bulk action bar. It lives outside selectedIds
|
|
1756
|
+
// so the row checkboxes can be visible even before anything is
|
|
1757
|
+
// ticked (otherwise the BulkActionBar's own "select all" button
|
|
1758
|
+
// is unreachable).
|
|
1759
|
+
const [selectModeEnabled, setSelectModeEnabled] = useState(false);
|
|
1760
|
+
// Closing the panel resets everything bulk-related.
|
|
1761
|
+
useEffect(() => {
|
|
1762
|
+
if (!open) {
|
|
1763
|
+
setSelectedIds(new Set());
|
|
1764
|
+
setLastClickedRank(null);
|
|
1765
|
+
setBulkPreview(null);
|
|
1766
|
+
setBulkProgress(null);
|
|
1767
|
+
setBulkResult(null);
|
|
1768
|
+
setBulkTagDraft(null);
|
|
1769
|
+
setBulkPresetDraft(null);
|
|
1770
|
+
setBulkPresetOptions([]);
|
|
1771
|
+
setBulkBusy(false);
|
|
1772
|
+
setSelectModeEnabled(false);
|
|
1773
|
+
}
|
|
1774
|
+
}, [open]);
|
|
1775
|
+
const toggleSelectMode = () => {
|
|
1776
|
+
setSelectModeEnabled(value => {
|
|
1777
|
+
if (value) clearSelection();
|
|
1778
|
+
return !value;
|
|
1779
|
+
});
|
|
1780
|
+
};
|
|
1781
|
+
const toggleSelect = (id, shift, currentIndex) => {
|
|
1782
|
+
setSelectedIds(prev => {
|
|
1783
|
+
const next = new Set(prev);
|
|
1784
|
+
if (shift && lastClickedRank !== null && Number.isInteger(currentIndex)) {
|
|
1785
|
+
const lo = Math.min(lastClickedRank, currentIndex);
|
|
1786
|
+
const hi = Math.max(lastClickedRank, currentIndex);
|
|
1787
|
+
const allInRange = rows.slice(lo, hi + 1).every(r => next.has(r.id));
|
|
1788
|
+
for (let i = lo; i <= hi; i++) {
|
|
1789
|
+
const rowId = rows[i] && rows[i].id;
|
|
1790
|
+
if (!rowId) continue;
|
|
1791
|
+
if (allInRange) next.delete(rowId); else next.add(rowId);
|
|
1792
|
+
}
|
|
1793
|
+
} else {
|
|
1794
|
+
if (next.has(id)) next.delete(id); else next.add(id);
|
|
1795
|
+
}
|
|
1796
|
+
return next;
|
|
1797
|
+
});
|
|
1798
|
+
if (!shift) setLastClickedRank(Number.isInteger(currentIndex) ? currentIndex : null);
|
|
1799
|
+
};
|
|
1800
|
+
// Clear the selected-id set without touching selectModeEnabled.
|
|
1801
|
+
// Earlier this also reset selectModeEnabled to false, which made
|
|
1802
|
+
// clicking the "清空选择" toolbar button silently exit bulk
|
|
1803
|
+
// mode. Bulk mode is now exited exclusively by toggleSelectMode()
|
|
1804
|
+
// (the "退出批量处理" toggle button in the toolbar).
|
|
1805
|
+
const clearSelection = () => { setSelectedIds(new Set()); setLastClickedRank(null); };
|
|
1806
|
+
const selectAllFiltered = () => {
|
|
1807
|
+
if (rows.length > 200) { setError(t("bulk.error.tooMany")); return; }
|
|
1808
|
+
setSelectedIds(new Set(rows.map(r => r.id)));
|
|
1809
|
+
};
|
|
1810
|
+
const selectedSessions = useMemo(() => {
|
|
1811
|
+
const out = [];
|
|
1812
|
+
for (const id of selectedIds) {
|
|
1813
|
+
const s = list.byId[id];
|
|
1814
|
+
if (!s) continue;
|
|
1815
|
+
out.push({ id, displayTitle: s.blank ? t("panel.session.new") : (s.displayTitle || id) });
|
|
1816
|
+
}
|
|
1817
|
+
return out;
|
|
1818
|
+
}, [selectedIds, list.byId, t]);
|
|
1819
|
+
const executeBulk = async ({ action, payload, actionLabel, ids, sessions }) => {
|
|
1820
|
+
if (ids.length === 0) return;
|
|
1821
|
+
if (ids.length > 200) { setError(t("bulk.error.tooMany")); return; }
|
|
1822
|
+
const cancelController = { abort: () => {} };
|
|
1823
|
+
setBulkProgress({ action, actionLabel, done: 0, total: ids.length, completed: false, cancel: () => cancelController.abort() });
|
|
1824
|
+
setBulkBusy(true);
|
|
1825
|
+
let result;
|
|
1826
|
+
try {
|
|
1827
|
+
result = await runBatch({ action, sessionIds: ids, payload }, ({ done }) => {
|
|
1828
|
+
setBulkProgress(prev => prev ? { ...prev, done, completed: false } : prev);
|
|
1829
|
+
});
|
|
1830
|
+
} catch (error) {
|
|
1831
|
+
setBulkProgress(null);
|
|
1832
|
+
setBulkBusy(false);
|
|
1833
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
const items = (result.items || []).map(item => {
|
|
1837
|
+
const session = sessions.find(s => s.id === item.sessionId);
|
|
1838
|
+
return { ...item, displayTitle: session ? session.displayTitle : (item.sessionId || "—") };
|
|
1839
|
+
});
|
|
1840
|
+
setBulkProgress(null);
|
|
1841
|
+
setBulkResult({ action, actionLabel, items });
|
|
1842
|
+
setBulkBusy(false);
|
|
1843
|
+
try { await annotationStore.load(true); } catch (_) { /* ignore */ }
|
|
1844
|
+
if (typeof props.onRefreshAfterBulk === "function") {
|
|
1845
|
+
try { await props.onRefreshAfterBulk(action); } catch (_) { /* ignore */ }
|
|
1846
|
+
}
|
|
1847
|
+
};
|
|
1848
|
+
const runBulk = ({ action, payload, actionLabel, needsConfirm }) => {
|
|
1849
|
+
if (bulkBusy) return;
|
|
1850
|
+
const ids = [...selectedIds];
|
|
1851
|
+
if (ids.length === 0) return;
|
|
1852
|
+
if (ids.length > 200) { setError(t("bulk.error.tooMany")); return; }
|
|
1853
|
+
const sessions = selectedSessions;
|
|
1854
|
+
const resolvedLabel = actionLabel || ({
|
|
1855
|
+
archive: t("bulk.archive"), unarchive: t("bulk.unarchive"), delete: t("bulk.delete"),
|
|
1856
|
+
move: t("bulk.move"), "preset-migrate": t("bulk.presetMigrate"),
|
|
1857
|
+
favorite: t("bulk.favorite"), unfavorite: t("bulk.unfavorite"),
|
|
1858
|
+
review: t("bulk.review"), unreview: t("bulk.unreview"),
|
|
1859
|
+
"add-tags": t("bulk.tagsAdd"), "remove-tags": t("bulk.tagsRemove"),
|
|
1860
|
+
"set-priority": t("bulk.priority")
|
|
1861
|
+
})[action] || action;
|
|
1862
|
+
if (needsConfirm) {
|
|
1863
|
+
setBulkPreview({ action, payload, actionLabel: resolvedLabel, sessions, warning: action === "delete" ? t("bulk.dialog.warning.delete") : "" });
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
void executeBulk({ action, payload, actionLabel: resolvedLabel, ids, sessions });
|
|
1867
|
+
};
|
|
1868
|
+
const retryFailedBulk = (failedIds, action, payload, actionLabel) => {
|
|
1869
|
+
setBulkResult(null);
|
|
1870
|
+
const filtered = (failedIds || []).filter(id => typeof id === "string" && id !== "");
|
|
1871
|
+
setSelectedIds(new Set(filtered));
|
|
1872
|
+
if (filtered.length === 0) return;
|
|
1873
|
+
const needsConfirm = action === "delete" || action === "move" || action === "preset-migrate" || action === "add-tags" || action === "remove-tags" || action === "set-priority";
|
|
1874
|
+
runBulk({ action, payload, actionLabel, needsConfirm });
|
|
1875
|
+
};
|
|
1876
|
+
const needsConfirmFor = (action) => action === "delete";
|
|
1877
|
+
const onBulkAction = (action, payload, forceNeedsConfirm) => {
|
|
1878
|
+
// Toggle: if a preview or result dialog is already open for this
|
|
1879
|
+
// same action, click the button again to close it.
|
|
1880
|
+
if (bulkPreview && bulkPreview.action === action) { setBulkPreview(null); return; }
|
|
1881
|
+
if (bulkResult && bulkResult.action === action) { setBulkResult(null); return; }
|
|
1882
|
+
const needsConfirm = forceNeedsConfirm === true || needsConfirmFor(action);
|
|
1883
|
+
runBulk({ action, payload, needsConfirm });
|
|
1884
|
+
};
|
|
1885
|
+
const onBulkSelectAll = () => { setSelectModeEnabled(true); selectAllFiltered(); };
|
|
1886
|
+
const onBulkClear = () => clearSelection();
|
|
1887
|
+
const onBulkTagsAdd = () => { if (bulkTagDraft && bulkTagDraft.mode === "add") { setBulkTagDraft(null); return; } setBulkTagDraft({ mode: "add" }); };
|
|
1888
|
+
const onBulkRemoveAllTags = () => onBulkAction("remove-tags", { tags: [] });
|
|
1889
|
+
const onBulkPriority = () => { if (bulkTagDraft && bulkTagDraft.mode === "priority") { setBulkTagDraft(null); return; } setBulkTagDraft({ mode: "priority" }); };
|
|
1890
|
+
const onBulkMove = () => { if (bulkTagDraft && bulkTagDraft.mode === "move") { setBulkTagDraft(null); return; } setBulkTagDraft({ mode: "move" }); };
|
|
1891
|
+
const onBulkTagApply = (tags) => {
|
|
1892
|
+
const mode = bulkTagDraft && bulkTagDraft.mode;
|
|
1893
|
+
setBulkTagDraft(null);
|
|
1894
|
+
if (mode === "add") onBulkAction("add-tags", { tags }, true);
|
|
1895
|
+
};
|
|
1896
|
+
const onBulkPriorityApply = (value) => {
|
|
1897
|
+
const priority = Number(value);
|
|
1898
|
+
setBulkTagDraft(null);
|
|
1899
|
+
if (Number.isInteger(priority) && priority >= 1 && priority <= 5) onBulkAction("set-priority", { priority }, true);
|
|
1900
|
+
};
|
|
1901
|
+
const onBulkMoveApply = (value) => {
|
|
1902
|
+
setBulkTagDraft(null);
|
|
1903
|
+
if (value) onBulkAction("move", { targetWorkspaceId: value }, true);
|
|
1904
|
+
};
|
|
1905
|
+
const [bulkPresetDraft, setBulkPresetDraft] = useState(null);
|
|
1906
|
+
const [bulkPresetOptions, setBulkPresetOptions] = useState([]);
|
|
1907
|
+
const onBulkPreset = () => {
|
|
1908
|
+
if (selectedIds.size === 0) return;
|
|
1909
|
+
// Toggle: close the dialog if it is already open.
|
|
1910
|
+
if (bulkPresetDraft) { setBulkPresetDraft(null); return; }
|
|
1911
|
+
|
|
1912
|
+
// Scan using the first selected session; /preset-scan returns
|
|
1913
|
+
// the global available preset list regardless of session, so any
|
|
1914
|
+
// selected id works.
|
|
1915
|
+
const firstId = [...selectedIds][0];
|
|
1916
|
+
setBulkPresetDraft({ mode: "preset" });
|
|
1917
|
+
setBulkPresetOptions([]);
|
|
1918
|
+
fetchApi("/preset-scan?sessionId=" + encodeURIComponent(firstId)).then((data) => {
|
|
1919
|
+
if (!data || !data.ok) return;
|
|
1920
|
+
const result = data.result || {};
|
|
1921
|
+
const presets = Array.isArray(result.availablePresets)
|
|
1922
|
+
? result.availablePresets
|
|
1923
|
+
: (result.rows && Array.isArray(result.rows.availablePresets) ? result.rows.availablePresets : []);
|
|
1924
|
+
setBulkPresetOptions(presets.filter(p => typeof p === "string" && p.length > 0).map(p => ({ value: p, label: p })));
|
|
1925
|
+
}).catch(() => { /* surface empty list */ });
|
|
1926
|
+
};
|
|
1927
|
+
const onBulkPresetApply = (value) => {
|
|
1928
|
+
setBulkPresetDraft(null);
|
|
1929
|
+
if (value) onBulkAction("preset-migrate", { toPreset: value }, true);
|
|
1930
|
+
};
|
|
1931
|
+
const bulkMoveOptions = useMemo(() => workspaces.map(ws => ({ value: ws.id, label: ws.title || ws.name || ws.id })), [workspaces]);
|
|
1932
|
+
const bulkPriorityOptions = useMemo(() => [1, 2, 3, 4, 5].map(value => ({ value: String(value), label: t("marks.priority." + value) })), [t]);
|
|
1933
|
+
useEffect(() => {
|
|
1934
|
+
const timer = setInterval(() => setNow(Date.now()), 30000);
|
|
1935
|
+
return () => clearInterval(timer);
|
|
1936
|
+
}, []);
|
|
1937
|
+
useEffect(() => {
|
|
1938
|
+
if (!open) return;
|
|
1939
|
+
let alive = true;
|
|
1940
|
+
const controller = new AbortController();
|
|
1941
|
+
setWorkspacesLoading(true);
|
|
1942
|
+
setWorkspacesError(null);
|
|
1943
|
+
fetchApi("/workspaces", { signal: controller.signal }).then((data) => {
|
|
1944
|
+
if (!data?.ok || !Array.isArray(data.result?.workspaces)) throw new Error(data?.error || "Invalid workspace response");
|
|
1945
|
+
const next = data.result.workspaces.filter(workspace => workspace && typeof workspace.id === "string");
|
|
1946
|
+
if (!alive) return;
|
|
1947
|
+
setWorkspaces(next);
|
|
1948
|
+
const times = data.result.sessionCreatedAt;
|
|
1949
|
+
setCreatedTimes(times && typeof times === "object" && !Array.isArray(times) ? times : {});
|
|
1950
|
+
setWorkspaceFilter(value => value.startsWith("workspace:") && !next.some(workspace => workspaceValue(workspace.id) === value) ? "all" : value);
|
|
1951
|
+
}).catch(error => {
|
|
1952
|
+
if (alive) setWorkspacesError(error instanceof Error ? error.message : String(error));
|
|
1953
|
+
}).finally(() => {
|
|
1954
|
+
if (alive) setWorkspacesLoading(false);
|
|
1955
|
+
});
|
|
1956
|
+
return () => { alive = false; controller.abort(); };
|
|
1957
|
+
}, [open, workspaceSnapshot, Boolean(moveFor), workspaceRefresh, sessionMembershipKey]);
|
|
1958
|
+
// Drain any pending action queued by a header button. The panel is
|
|
1959
|
+
// the single owner of every inner dialog, so opening it with a
|
|
1960
|
+
// pending action pre-populates the corresponding dialog state.
|
|
1961
|
+
useEffect(() => {
|
|
1962
|
+
if (!open) return;
|
|
1963
|
+
const pending = panelStore.consumePendingMove();
|
|
1964
|
+
if (pending && pending.sessionId) {
|
|
1965
|
+
setMoveFor({ id: pending.sessionId, displayTitle: pending.displayTitle || pending.sessionId, workspaceId: pending.workspaceId || "" });
|
|
1966
|
+
}
|
|
1967
|
+
}, [open]);
|
|
1968
|
+
const archived = useMemo(() => new Set(archivedIds), [archivedIds]);
|
|
1969
|
+
const workspaceIndex = useMemo(() => indexWorkspaces(workspaces), [workspaces]);
|
|
1970
|
+
const allRows = useMemo(() => list.ids.map(id => list.byId[id])
|
|
1971
|
+
.filter(session => session != null && session.origin !== "subagent" && (!session.blank || session.id === list.current)), [list]);
|
|
1972
|
+
const availableTags = useMemo(() => {
|
|
1973
|
+
const tags = new Map();
|
|
1974
|
+
for (const session of allRows) for (const tag of annotationOf(annotations, session.id).tags) if (!tags.has(tag.toLowerCase())) tags.set(tag.toLowerCase(), tag);
|
|
1975
|
+
return [...tags].sort((a, b) => a[1].localeCompare(b[1]));
|
|
1976
|
+
}, [allRows, annotations.entries]);
|
|
1977
|
+
useEffect(() => {
|
|
1978
|
+
// A removed last tag must not leave an invisible active filter while
|
|
1979
|
+
// the select falls back to displaying "All tags".
|
|
1980
|
+
if (annotations.loaded && !annotations.error && tagFilter.startsWith("tag:")
|
|
1981
|
+
&& !availableTags.some(([key]) => key === tagFilter.slice(4))) setTagFilter("all");
|
|
1982
|
+
}, [availableTags, tagFilter, annotations.loaded, annotations.error]);
|
|
1983
|
+
const rows = useMemo(() => {
|
|
1984
|
+
const needle = query.trim().toLowerCase();
|
|
1985
|
+
const selected = allRows.filter(session => {
|
|
1986
|
+
if (filter === "active" && archived.has(session.id)) return false;
|
|
1987
|
+
if (filter === "archived" && !archived.has(session.id)) return false;
|
|
1988
|
+
const mark = annotationOf(annotations, session.id);
|
|
1989
|
+
if (onlyFavorite && !mark.favorite || onlyReview && !mark.reviewLater) return false;
|
|
1990
|
+
if (tagFilter === "none" && mark.tags.length !== 0) return false;
|
|
1991
|
+
if (tagFilter.startsWith("tag:") && !mark.tags.some(tag => tag.toLowerCase() === tagFilter.slice(4))) return false;
|
|
1992
|
+
if (priorityFilter !== "all" && (mark.priority ?? 3) !== Number(priorityFilter)) return false;
|
|
1993
|
+
const title = session.blank ? t("panel.session.new") : session.displayTitle || session.id;
|
|
1994
|
+
if (needle && ![title, session.id, mark.note, ...mark.tags].some(value => value.toLowerCase().includes(needle))) return false;
|
|
1995
|
+
if (workspaceFilter !== "all") {
|
|
1996
|
+
const memberships = rowWorkspaces(session, workspaceIndex);
|
|
1997
|
+
if (workspaceFilter === "ungrouped" ? memberships.size !== 0 : !memberships.has(workspaceFilter.slice("workspace:".length))) return false;
|
|
1998
|
+
}
|
|
1999
|
+
return true;
|
|
2000
|
+
});
|
|
2001
|
+
const field = sortOrder.startsWith("created-") ? "createdAt" : "updatedAt";
|
|
2002
|
+
const direction = sortOrder === "priority" || sortOrder.endsWith("-asc") ? 1 : -1;
|
|
2003
|
+
// Sort a derived array only. Equal timestamps preserve source order;
|
|
2004
|
+
// missing/invalid timestamps stay last in either direction. Priority
|
|
2005
|
+
// is normalized to 1-5 (legacy null treated as 3) so unset rows sort
|
|
2006
|
+
// alongside priority 3.
|
|
2007
|
+
const timeFor = session => {
|
|
2008
|
+
if (sortOrder === "priority") return annotationOf(annotations, session.id).priority ?? 3;
|
|
2009
|
+
return field === "createdAt" ? sessionTime(session.createdAt) ?? sessionTime(createdTimes[session.id]) : sessionTime(session.updatedAt);
|
|
2010
|
+
};
|
|
2011
|
+
return selected.map((session, rank) => ({ session, rank, time: timeFor(session) }))
|
|
2012
|
+
.sort((a, b) => a.time === b.time ? a.rank - b.rank : a.time === null ? 1 : b.time === null ? -1 : direction * (a.time - b.time))
|
|
2013
|
+
.map(item => item.session);
|
|
2014
|
+
}, [allRows, archived, filter, query, workspaceFilter, workspaceIndex, sortOrder, createdTimes, annotations.entries, onlyFavorite, onlyReview, tagFilter, priorityFilter, t]);
|
|
2015
|
+
// The set of ids that survive the current filter; bulk-selection
|
|
2016
|
+
// bookkeeping reads from this so it stays in sync with row visibility.
|
|
2017
|
+
const currentFilterIds = useMemo(() => rows.map(r => r.id), [rows]);
|
|
2018
|
+
const currentFilterRank = useMemo(() => {
|
|
2019
|
+
const m = new Map();
|
|
2020
|
+
rows.forEach((r, i) => m.set(r.id, i));
|
|
2021
|
+
return m;
|
|
2022
|
+
}, [rows]);
|
|
2023
|
+
// Drop ids from the selection that no longer match the current filter
|
|
2024
|
+
// (e.g. user narrowed the workspace filter or hid archived rows).
|
|
2025
|
+
useEffect(() => {
|
|
2026
|
+
const allowed = new Set(currentFilterIds);
|
|
2027
|
+
setSelectedIds(prev => {
|
|
2028
|
+
let changed = false;
|
|
2029
|
+
const next = new Set();
|
|
2030
|
+
for (const id of prev) if (allowed.has(id)) next.add(id); else changed = true;
|
|
2031
|
+
return changed ? next : prev;
|
|
2032
|
+
});
|
|
2033
|
+
}, [currentFilterIds]);
|
|
2034
|
+
const creationTimeUnavailable = sortOrder.startsWith("created-") && allRows.length > 0
|
|
2035
|
+
&& !allRows.some(session => (sessionTime(session.createdAt) ?? sessionTime(createdTimes[session.id])) !== null);
|
|
2036
|
+
const hasViewChanges = onlyFavorite || onlyReview || tagFilter !== "all" || priorityFilter !== "all" || query !== "" || filter !== "all" || workspaceFilter !== "all" || sortOrder !== DEFAULT_SORT;
|
|
2037
|
+
const resetView = () => { setQuery(""); setFilter("all"); setWorkspaceFilter("all"); setSortOrder(DEFAULT_SORT); setOnlyFavorite(false); setOnlyReview(false); setTagFilter("all"); setPriorityFilter("all"); };
|
|
2038
|
+
const runRow = async (sessionId, fn) => {
|
|
2039
|
+
if (busyId !== null) return;
|
|
2040
|
+
setBusyId(sessionId);
|
|
2041
|
+
setError(null);
|
|
2042
|
+
try {
|
|
2043
|
+
await fn();
|
|
2044
|
+
} catch (e) {
|
|
2045
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
2046
|
+
} finally {
|
|
2047
|
+
setBusyId(null);
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
2050
|
+
const filterBtn = (id, label) => h("button", {
|
|
2051
|
+
type: "button",
|
|
2052
|
+
key: id,
|
|
2053
|
+
className: "sm-filterBtn" + (filter === id ? " sm-on" : ""),
|
|
2054
|
+
"aria-pressed": filter === id,
|
|
2055
|
+
onClick: () => setFilter(id)
|
|
2056
|
+
}, label);
|
|
2057
|
+
const renderRow = (s, rank) => {
|
|
2058
|
+
const isArchived = archived.has(s.id);
|
|
2059
|
+
const isCurrent = s.id === list.current;
|
|
2060
|
+
const busy = busyId === s.id;
|
|
2061
|
+
const mark = annotationOf(annotations, s.id);
|
|
2062
|
+
const isSelected = selectedIds.has(s.id);
|
|
2063
|
+
const checkbox = (selectedIds.size === 0 && !selectModeEnabled)
|
|
2064
|
+
? null
|
|
2065
|
+
: h("label", { className: "sm-bulkRowCheckbox", onClick: (e) => e.stopPropagation() },
|
|
2066
|
+
h("input", {
|
|
2067
|
+
type: "checkbox",
|
|
2068
|
+
checked: isSelected,
|
|
2069
|
+
disabled: busy || bulkBusy,
|
|
2070
|
+
"aria-label": t("bulk.checkbox.aria", { title: s.displayTitle || s.id }),
|
|
2071
|
+
onChange: (e) => toggleSelect(s.id, e.shiftKey === true, rank)
|
|
2072
|
+
})
|
|
2073
|
+
);
|
|
2074
|
+
return h("div", {
|
|
2075
|
+
key: s.id,
|
|
2076
|
+
"data-session-id": s.id,
|
|
2077
|
+
className: "sm-row" + (isCurrent ? " sm-rowCurrent" : "") + (isSelected ? " sm-rowBulkSelected" : "")
|
|
2078
|
+
},
|
|
2079
|
+
h("div", { className: "sm-rowCheckboxSlot" + ((selectedIds.size > 0 || selectModeEnabled) ? " sm-rowCheckboxSlot-visible" : "") }, checkbox),
|
|
2080
|
+
h("div", { className: "sm-rowMain" },
|
|
2081
|
+
h("div", { className: "sm-rowTitle" },
|
|
2082
|
+
h("span", null, s.blank ? t("panel.session.new") : (s.displayTitle || s.id)),
|
|
2083
|
+
isArchived ? h("span", { className: "sm-badge sm-badgeArchived" }, t("panel.archived")) : null,
|
|
2084
|
+
s.running ? h("span", { className: "sm-badge sm-badgeRunning" }, t("panel.running")) : null,
|
|
2085
|
+
isCurrent ? h("span", { className: "sm-badge sm-badgeCurrent" }, t("panel.current")) : null
|
|
2086
|
+
),
|
|
2087
|
+
h("div", { className: "sm-rowSecondary" },
|
|
2088
|
+
h("div", { className: "sm-rowBadges" },
|
|
2089
|
+
h("span", { className: "sm-priorityBadge sm-priority-" + (mark.priority ?? 3), title: t("marks.priority." + (mark.priority ?? 3)) }, "P" + (mark.priority ?? 3)),
|
|
2090
|
+
mark.tags.slice(0, 2).map(tag => h("button", { key: tag, type: "button", className: "sm-tagChip", title: tag, onClick: () => setTagFilter("tag:" + tag.toLowerCase()) }, tag)),
|
|
2091
|
+
mark.tags.length > 2 ? h("span", { title: mark.tags.join(", ") }, "+" + (mark.tags.length - 2)) : null,
|
|
2092
|
+
mark.note ? h("button", { type: "button", className: "sm-noteBadge", title: t("marks.note.exists"), onClick: () => { setAnnotationAnchor(null); setAnnotationFor(current => current?.id === s.id ? null : s); } }, "✎") : null
|
|
2093
|
+
),
|
|
2094
|
+
h("div", { className: "sm-rowMeta" }, shortPath(s.cwd) + (s.cwd ? " · " : "") + formatRelative(s.updatedAt, now)),
|
|
2095
|
+
h("div", { className: "sm-rowActions" },
|
|
2096
|
+
h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => onOpen(s.id) }, t("row.open")),
|
|
2097
|
+
h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => runRow(s.id, () => isArchived ? onUnarchive(s.id) : onArchive(s.id)) }, t(isArchived ? "row.unarchive" : "row.archive")),
|
|
2098
|
+
annotationButtons(s.id, s.displayTitle || s.id, t, annotations, "sm-rowBtn", patch => runRow(s.id, () => annotationStore.save(s.id, patch)), () => { setAnnotationAnchor(null); setAnnotationFor(current => current?.id === s.id ? null : s); }, busy),
|
|
2099
|
+
h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => { if (moveFor && moveFor.id === s.id) setMoveFor(null); else setMoveFor(s); } }, t("row.move")),
|
|
2100
|
+
h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => { if (migrateFor && migrateFor.id === s.id) setMigrateFor(null); else setMigrateFor(s); } }, t("row.migrate")),
|
|
2101
|
+
h("button", { type: "button", className: "sm-rowBtn sm-rowBtnDanger", disabled: busy, onClick: () => { if (confirmFor && confirmFor.id === s.id) setConfirmFor(null); else setConfirmFor(s); } }, t("row.delete"))
|
|
2102
|
+
)
|
|
2103
|
+
)
|
|
2104
|
+
)
|
|
2105
|
+
);
|
|
2106
|
+
};
|
|
2107
|
+
const rowsList = rows.length === 0
|
|
2108
|
+
? h("div", { className: "sm-empty" }, allRows.length > 0 ? t("panel.empty.filtered") : filter === "archived" ? t("panel.empty.archived") : t("panel.empty"))
|
|
2109
|
+
: h("div", { className: "sm-list" }, rows.map((s, i) => renderRow(s, i)));
|
|
2110
|
+
const onPanelClose = () => {
|
|
2111
|
+
if (busyId !== null) return;
|
|
2112
|
+
setConfirmFor(null);
|
|
2113
|
+
setMoveFor(null);
|
|
2114
|
+
setMigrateFor(null);
|
|
2115
|
+
onClose();
|
|
2116
|
+
};
|
|
2117
|
+
// Esc is handled at window level (issue #7): the original layer-
|
|
2118
|
+
// bound onKeyDown was unreachable when focus stayed on the sidebar
|
|
2119
|
+
// toggle button that opened the panel -- that button is a sibling
|
|
2120
|
+
// of the panel layer, so keydown never bubbles into the panel
|
|
2121
|
+
// subtree. The state ref lets the listener see the latest values
|
|
2122
|
+
// without re-subscribing on every render. The panel listener
|
|
2123
|
+
// returns early when any inner dialog is open so the child
|
|
2124
|
+
// dialog's own window listener gets the first shot at Esc.
|
|
2125
|
+
const escStateRef = useRef({});
|
|
2126
|
+
escStateRef.current = { busyId, confirmFor, moveFor, migrateFor, annotationFor, onPanelClose };
|
|
2127
|
+
useEffect(() => {
|
|
2128
|
+
if (!open) return;
|
|
2129
|
+
const handler = (e) => {
|
|
2130
|
+
if (e.key !== "Escape") return;
|
|
2131
|
+
if (e.defaultPrevented) return;
|
|
2132
|
+
if (e.isComposing || e.keyCode === 229) return;
|
|
2133
|
+
const s = escStateRef.current;
|
|
2134
|
+
if (s.busyId !== null) return;
|
|
2135
|
+
if (s.confirmFor || s.moveFor || s.migrateFor || s.annotationFor) return;
|
|
2136
|
+
e.preventDefault();
|
|
2137
|
+
e.stopPropagation();
|
|
2138
|
+
s.onPanelClose();
|
|
2139
|
+
// Esc after a keyboard-driven close leaves the originally-
|
|
2140
|
+
// focused trigger button (sidebar toggle / row button) with
|
|
2141
|
+
// :focus-visible still active -- blur it so no focus ring
|
|
2142
|
+
// lingers after the panel closes. Mouse-driven closes are
|
|
2143
|
+
// not affected: :focus-visible only activates for keyboard
|
|
2144
|
+
// focus, so the ring never shows in that path.
|
|
2145
|
+
const ae = document.activeElement;
|
|
2146
|
+
if (ae && typeof ae.blur === "function" && ae !== document.body) ae.blur();
|
|
2147
|
+
};
|
|
2148
|
+
window.addEventListener("keydown", handler);
|
|
2149
|
+
return () => window.removeEventListener("keydown", handler);
|
|
2150
|
+
}, [open]);
|
|
2151
|
+
const panelModal = h("div", {
|
|
2152
|
+
className: "sm-nativeDialogLayer",
|
|
2153
|
+
role: "presentation"
|
|
2154
|
+
},
|
|
2155
|
+
h("section", {
|
|
2156
|
+
className: "sm-nativeDialog sm-panelDialog",
|
|
2157
|
+
role: "dialog",
|
|
2158
|
+
"aria-modal": "true",
|
|
2159
|
+
"aria-labelledby": "sm-panel-dialog-title"
|
|
2160
|
+
},
|
|
2161
|
+
h("div", { className: "sm-nativeDialogHeader" },
|
|
2162
|
+
h("a", {
|
|
2163
|
+
href: "https://github.com/hkkz9522/dsh-session-manager",
|
|
2164
|
+
target: "_blank",
|
|
2165
|
+
rel: "noopener noreferrer",
|
|
2166
|
+
className: "sm-panelHeaderGithub",
|
|
2167
|
+
title: t("panel.github.title"),
|
|
2168
|
+
"aria-label": t("panel.github.title")
|
|
2169
|
+
},
|
|
2170
|
+
h("svg", { viewBox: "0 0 16 16", width: 16, height: 16, "aria-hidden": "true" },
|
|
2171
|
+
h("path", {
|
|
2172
|
+
fill: "currentColor",
|
|
2173
|
+
d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"
|
|
2174
|
+
})
|
|
2175
|
+
)
|
|
2176
|
+
),
|
|
2177
|
+
h("h2", { id: "sm-panel-dialog-title", className: "sm-nativeDialogTitle" }, t("panel.title")),
|
|
2178
|
+
h("div", { className: "sm-nativeDialogHeaderActions" },
|
|
2179
|
+
h("button", {
|
|
2180
|
+
type: "button",
|
|
2181
|
+
className: "sm-panelHeaderClose",
|
|
2182
|
+
disabled: busyId !== null || migrating,
|
|
2183
|
+
onClick: onPanelClose
|
|
2184
|
+
}, t("panel.close"))
|
|
2185
|
+
)
|
|
2186
|
+
),
|
|
2187
|
+
h("div", { className: "sm-panel" },
|
|
2188
|
+
h("div", { className: "sm-panelTools" },
|
|
2189
|
+
h("input", {
|
|
2190
|
+
type: "search", className: "sm-queryInput", value: query,
|
|
2191
|
+
placeholder: t("panel.search"), "aria-label": t("panel.search"), autoComplete: "off",
|
|
2192
|
+
onChange: event => setQuery(event.target.value)
|
|
2193
|
+
}),
|
|
2194
|
+
h("div", { className: "sm-panelSelects" },
|
|
2195
|
+
h("label", { className: "sm-viewField" },
|
|
2196
|
+
h("span", null, t("panel.workspace")),
|
|
2197
|
+
h("select", {
|
|
2198
|
+
className: "sm-viewSelect", "aria-label": t("panel.workspace"), value: workspaceFilter,
|
|
2199
|
+
disabled: workspacesLoading || workspacesError !== null,
|
|
2200
|
+
onChange: event => setWorkspaceFilter(event.target.value)
|
|
2201
|
+
},
|
|
2202
|
+
h("option", { value: "all" }, t("panel.workspace.all")),
|
|
2203
|
+
h("option", { value: "ungrouped" }, t("panel.workspace.ungrouped")),
|
|
2204
|
+
workspaces.map(workspace => h("option", { key: workspace.id, value: workspaceValue(workspace.id), title: workspace.path || workspace.id }, workspace.title || workspace.name || workspace.id))
|
|
2205
|
+
)
|
|
2206
|
+
),
|
|
2207
|
+
h("label", { className: "sm-viewField" },
|
|
2208
|
+
h("span", null, t("panel.sort")),
|
|
2209
|
+
h("select", {
|
|
2210
|
+
className: "sm-viewSelect", "aria-label": t("panel.sort"), value: sortOrder,
|
|
2211
|
+
onChange: event => setSortOrder(event.target.value)
|
|
2212
|
+
}, SORT_ORDERS.map(value => h("option", { key: value, value, disabled: value === "priority" && !marksReady }, t(value === "priority" ? "marks.sort" : "panel.sort." + value))))
|
|
2213
|
+
),
|
|
2214
|
+
h("label", { className: "sm-viewField" },
|
|
2215
|
+
h("span", null, t("marks.tags")),
|
|
2216
|
+
h("select", { className: "sm-viewSelect", "aria-label": t("marks.filter.tags"), value: tagFilter, disabled: !marksReady, onChange: e => setTagFilter(e.target.value) },
|
|
2217
|
+
h("option", { value: "all" }, t("marks.tags.all")), h("option", { value: "none" }, t("marks.tags.none")),
|
|
2218
|
+
availableTags.map(([key, tag]) => h("option", { key, value: "tag:" + key }, tag)),
|
|
2219
|
+
tagFilter.startsWith("tag:") && !availableTags.some(([key]) => "tag:" + key === tagFilter) ? h("option", { value: tagFilter }, t("marks.tags.missing", { tag: tagFilter.slice(4) })) : null
|
|
2220
|
+
)
|
|
2221
|
+
),
|
|
2222
|
+
h("label", { className: "sm-viewField" },
|
|
2223
|
+
h("span", null, t("marks.priority")),
|
|
2224
|
+
h("select", { className: "sm-viewSelect", "aria-label": t("marks.filter.priority"), value: priorityFilter, disabled: !marksReady, onChange: e => setPriorityFilter(e.target.value) },
|
|
2225
|
+
h("option", { value: "all" }, t("marks.priority.all")),
|
|
2226
|
+
[1,2,3,4,5].map(value => h("option", { key: value, value: String(value) }, t("marks.priority." + value)))
|
|
2227
|
+
)
|
|
2228
|
+
)
|
|
2229
|
+
),
|
|
2230
|
+
h("div", { className: "sm-markFilters" },
|
|
2231
|
+
h("div", { className: "sm-markFiltersGroup" },
|
|
2232
|
+
filterBtn("all", t("panel.filter.all")),
|
|
2233
|
+
filterBtn("active", t("panel.filter.active")),
|
|
2234
|
+
filterBtn("archived", t("panel.filter.archived")),
|
|
2235
|
+
h("button", { type: "button", className: "sm-filterBtn sm-markFilterToggle" + (onlyFavorite ? " sm-on" : ""), "aria-label": t("marks.filter.favorite.aria"), "aria-pressed": onlyFavorite, disabled: !marksReady, onClick: () => setOnlyFavorite(value => !value) }, "★ " + t("marks.filter.favorite")),
|
|
2236
|
+
h("button", { type: "button", className: "sm-filterBtn sm-markFilterToggle" + (onlyReview ? " sm-on" : ""), "aria-label": t("marks.filter.review.aria"), "aria-pressed": onlyReview, disabled: !marksReady, onClick: () => setOnlyReview(value => !value) }, "◷ " + t("marks.filter.review"))
|
|
2237
|
+
),
|
|
2238
|
+
h("div", { className: "sm-markFiltersSpacer" }),
|
|
2239
|
+
h("div", { className: "sm-markFiltersGroup" },
|
|
2240
|
+
h("button", {
|
|
2241
|
+
type: "button",
|
|
2242
|
+
className: "sm-filterBtn" + (selectModeEnabled ? " sm-on" : ""),
|
|
2243
|
+
disabled: busyId !== null || migrating,
|
|
2244
|
+
"aria-pressed": selectModeEnabled,
|
|
2245
|
+
"aria-label": t(selectModeEnabled ? "panel.selectMode.active.aria" : "panel.selectMode.aria"),
|
|
2246
|
+
onClick: toggleSelectMode
|
|
2247
|
+
}, selectModeEnabled ? t("panel.selectMode.exit") : t("panel.selectMode")),
|
|
2248
|
+
h("button", { type: "button", className: "sm-filterBtn sm-bulkSelectBtn", disabled: rows.length === 0 || rows.length === selectedIds.size || bulkBusy, onClick: onBulkSelectAll, "aria-label": t("bulk.selectAllFiltered") }, t("bulk.selectAllFiltered")),
|
|
2249
|
+
h("button", { type: "button", className: "sm-filterBtn sm-bulkSelectBtn", disabled: selectedIds.size === 0 || bulkBusy, onClick: onBulkClear, "aria-label": t("bulk.clear") }, t("bulk.clear"))
|
|
2250
|
+
)
|
|
2251
|
+
),
|
|
2252
|
+
h("div", { className: "sm-resultSummary" },
|
|
2253
|
+
h("span", { role: "status", "aria-live": "polite", "aria-atomic": "true" }, t("panel.count", { shown: rows.length, total: allRows.length })),
|
|
2254
|
+
hasViewChanges ? h("button", { type: "button", className: "sm-resetFilters", onClick: resetView }, t("panel.reset")) : null
|
|
2255
|
+
),
|
|
2256
|
+
annotations.loading && !annotations.loaded ? h("div", { className: "sm-workspaceNotice", role: "status" }, t("marks.loading")) : null,
|
|
2257
|
+
annotations.error ? h("div", { className: "sm-workspaceNotice", role: "alert" }, t("marks.error", { message: annotations.error }), h("button", { type: "button", className: "sm-resetFilters", onClick: () => annotationStore.load(true) }, t("marks.retry"))) : null,
|
|
2258
|
+
creationTimeUnavailable && !workspacesLoading ? h("div", { className: "sm-workspaceNotice", role: "status" }, t("panel.sort.unavailable")) : null,
|
|
2259
|
+
workspacesLoading ? h("div", { className: "sm-workspaceNotice", role: "status" }, t("panel.workspace.loading")) : null,
|
|
2260
|
+
workspacesError ? h("div", { className: "sm-workspaceNotice", role: "alert" },
|
|
2261
|
+
h("span", null, t("panel.workspace.error", { message: workspacesError })),
|
|
2262
|
+
h("button", { type: "button", className: "sm-resetFilters", onClick: () => setWorkspaceRefresh(value => value + 1) }, t("panel.workspace.retry"))
|
|
2263
|
+
) : null
|
|
2264
|
+
),
|
|
2265
|
+
error ? h("div", { className: "sm-error", role: "alert" }, t("error.operation", { message: error })) : null,
|
|
2266
|
+
(selectedIds.size > 0 || selectModeEnabled) ? h(BulkActionBar, {
|
|
2267
|
+
t, selectedCount: selectedIds.size, filteredCount: rows.length, busy: bulkBusy,
|
|
2268
|
+
onSelectAll: onBulkSelectAll, onClear: onBulkClear,
|
|
2269
|
+
onAction: onBulkAction,
|
|
2270
|
+
onOpenTagDialog: onBulkTagsAdd,
|
|
2271
|
+
onOpenPriorityDialog: onBulkPriority,
|
|
2272
|
+
onOpenMoveDialog: onBulkMove,
|
|
2273
|
+
onOpenPresetDialog: onBulkPreset
|
|
2274
|
+
}) : null,
|
|
2275
|
+
rowsList
|
|
2276
|
+
)
|
|
2277
|
+
)
|
|
2278
|
+
);
|
|
2279
|
+
|
|
2280
|
+
const deleteDialog = confirmFor === null ? null : h(ConfirmDialog, {
|
|
2281
|
+
open: true,
|
|
2282
|
+
onClose: () => { if (busyId === null) setConfirmFor(null); },
|
|
2283
|
+
title: t("confirm.delete.title"),
|
|
2284
|
+
description: t("confirm.delete.desc", { title: confirmFor.displayTitle || confirmFor.id }),
|
|
2285
|
+
warning: confirmFor.running ? t("confirm.delete.running") : null,
|
|
2286
|
+
confirmLabel: t("confirm.delete.confirm"),
|
|
2287
|
+
cancelLabel: t("confirm.cancel"),
|
|
2288
|
+
busy: busyId !== null,
|
|
2289
|
+
danger: true,
|
|
2290
|
+
onCancel: () => { if (busyId === null) setConfirmFor(null); },
|
|
2291
|
+
onConfirm: () => {
|
|
2292
|
+
const target = confirmFor;
|
|
2293
|
+
setConfirmFor(null);
|
|
2294
|
+
runRow(target.id, () => onRemove(target.id));
|
|
2295
|
+
}
|
|
2296
|
+
});
|
|
2297
|
+
const moveDialog = moveFor === null ? null : h(MoveDialog, {
|
|
2298
|
+
open: true,
|
|
2299
|
+
onClose: () => { if (busyId === null) setMoveFor(null); },
|
|
2300
|
+
title: t("confirm.move.title"),
|
|
2301
|
+
description: t("confirm.move.desc", { title: moveFor.displayTitle || moveFor.id }),
|
|
2302
|
+
sessionTitle: moveFor.displayTitle || moveFor.id,
|
|
2303
|
+
currentWorkspaceId: moveFor.workspaceId,
|
|
2304
|
+
workspaces: workspaces,
|
|
2305
|
+
confirmLabel: t("confirm.move.confirm"),
|
|
2306
|
+
cancelLabel: t("confirm.cancel"),
|
|
2307
|
+
busy: busyId !== null,
|
|
2308
|
+
onCancel: () => { if (busyId === null) setMoveFor(null); },
|
|
2309
|
+
onConfirm: (targetWorkspaceId) => {
|
|
2310
|
+
const target = moveFor;
|
|
2311
|
+
setMoveFor(null);
|
|
2312
|
+
runRow(target.id, async () => {
|
|
2313
|
+
await onMove(target.id, targetWorkspaceId);
|
|
2314
|
+
setWorkspaceRefresh(value => value + 1);
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2317
|
+
});
|
|
2318
|
+
const migrateDialog = migrateFor === null ? null : h(MigratePresetDialog, {
|
|
2319
|
+
open: true,
|
|
2320
|
+
t: t,
|
|
2321
|
+
sessionId: migrateFor.id,
|
|
2322
|
+
currentPreset: migrateFor.agentPreset || "",
|
|
2323
|
+
onCancel: () => { if (!migrating) setMigrateFor(null); },
|
|
2324
|
+
busy: migrating,
|
|
2325
|
+
onRun: async (targetPreset) => {
|
|
2326
|
+
const target = migrateFor;
|
|
2327
|
+
setMigrating(true);
|
|
2328
|
+
setError(null);
|
|
2329
|
+
try {
|
|
2330
|
+
await onMigrate(target.id, targetPreset);
|
|
2331
|
+
setMigrateFor(null);
|
|
2332
|
+
try { window.alert(t("migrate.completed", { preset: targetPreset })); } catch (_) {}
|
|
2333
|
+
} catch (error) {
|
|
2334
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
2335
|
+
setMigrateFor(null);
|
|
2336
|
+
} finally {
|
|
2337
|
+
setMigrating(false);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
});
|
|
2341
|
+
// Render the panel AND the inner dialog as siblings so React keeps
|
|
2342
|
+
// both mounted. CSS z-index keeps the right ordering: the inner
|
|
2343
|
+
// dialog (.sm-confirmDialog / .sm-moveDialog / .sm-migrateDialog)
|
|
2344
|
+
// carries z-index 10000 and therefore sits ON TOP OF the panel
|
|
2345
|
+
// (z-index 9999). Clicking inside the panel that the inner dialog
|
|
2346
|
+
// blocks is not possible because the inner backdrop covers the
|
|
2347
|
+
// panel area entirely (the inner modal is centered).
|
|
2348
|
+
const annotationDialog = annotationFor ? h(AnnotationDialog, { key: annotationFor.id, sessionId: annotationFor.id, displayTitle: annotationFor.displayTitle || annotationFor.id, t, anchor: annotationAnchor, onClose: () => { setAnnotationFor(null); setAnnotationAnchor(null); } }) : null;
|
|
2349
|
+
// Render panel + ALL inner dialogs together; z-index ordering puts
|
|
2350
|
+
// whichever dialog is open above the panel. The bulk dialogs are
|
|
2351
|
+
// appended here so destructive actions get a preview, in-flight
|
|
2352
|
+
// operations get a progress bar, and finished runs show per-id
|
|
2353
|
+
// success / failed / skipped counts.
|
|
2354
|
+
// Map bulk action -> 4-char confirm label ("确认<verb>"). Delete is
|
|
2355
|
+
// the only destructive action routed through BatchPreviewDialog, so it
|
|
2356
|
+
// also gets danger: true so the confirm button paints red.
|
|
2357
|
+
const bulkConfirmLabelFor = (action) => ({
|
|
2358
|
+
archive: t("bulk.confirm.archive"),
|
|
2359
|
+
unarchive: t("bulk.confirm.unarchive"),
|
|
2360
|
+
favorite: t("bulk.confirm.favorite"),
|
|
2361
|
+
unfavorite: t("bulk.confirm.unfavorite"),
|
|
2362
|
+
review: t("bulk.confirm.review"),
|
|
2363
|
+
unreview: t("bulk.confirm.unreview"),
|
|
2364
|
+
"add-tags": t("bulk.confirm.tagsAdd"),
|
|
2365
|
+
"remove-tags": t("bulk.confirm.tagsRemove"),
|
|
2366
|
+
"set-priority": t("bulk.confirm.priority"),
|
|
2367
|
+
move: t("bulk.confirm.move"),
|
|
2368
|
+
"preset-migrate": t("bulk.confirm.presetMigrate"),
|
|
2369
|
+
delete: t("bulk.confirm.delete")
|
|
2370
|
+
})[action] || t("bulk.dialog.execute");
|
|
2371
|
+
const bulkPreviewDialog = bulkPreview ? h(BatchPreviewDialog, {
|
|
2372
|
+
open: true, t, actionLabel: bulkPreview.actionLabel,
|
|
2373
|
+
sessions: bulkPreview.sessions, warning: bulkPreview.warning,
|
|
2374
|
+
confirmLabel: bulkConfirmLabelFor(bulkPreview.action),
|
|
2375
|
+
danger: bulkPreview.action === "delete",
|
|
2376
|
+
onCancel: () => setBulkPreview(null),
|
|
2377
|
+
onExecute: () => {
|
|
2378
|
+
const preview = bulkPreview;
|
|
2379
|
+
setBulkPreview(null);
|
|
2380
|
+
void executeBulk({
|
|
2381
|
+
action: preview.action, payload: preview.payload,
|
|
2382
|
+
actionLabel: preview.actionLabel,
|
|
2383
|
+
ids: preview.sessions.map(s => s.id), sessions: preview.sessions
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
}) : null;
|
|
2387
|
+
const bulkProgressDialog = bulkProgress ? h(BatchProgressDialog, {
|
|
2388
|
+
open: true, t, actionLabel: bulkProgress.actionLabel,
|
|
2389
|
+
done: bulkProgress.done, total: bulkProgress.total, completed: false,
|
|
2390
|
+
onCancel: () => { try { bulkProgress.cancel(); } catch (_) {} }
|
|
2391
|
+
}) : null;
|
|
2392
|
+
const bulkResultDialog = bulkResult ? h(BatchResultDialog, {
|
|
2393
|
+
open: true, t, actionLabel: bulkResult.actionLabel,
|
|
2394
|
+
items: bulkResult.items,
|
|
2395
|
+
onClose: () => { setBulkResult(null); clearSelection(); },
|
|
2396
|
+
onRetry: (failedIds) => {
|
|
2397
|
+
const last = bulkResult;
|
|
2398
|
+
setBulkResult(null);
|
|
2399
|
+
retryFailedBulk(failedIds, last.action, last.payload, last.actionLabel);
|
|
2400
|
+
}
|
|
2401
|
+
}) : null;
|
|
2402
|
+
const bulkTagDialog = bulkTagDraft && bulkTagDraft.mode === "add" ? h(BulkTagInputDialog, {
|
|
2403
|
+
open: true, t, title: t("bulk.tagsAdd"),
|
|
2404
|
+
confirmLabel: t("bulk.confirm.tagsAdd"),
|
|
2405
|
+
onCancel: () => setBulkTagDraft(null),
|
|
2406
|
+
onApply: onBulkTagApply
|
|
2407
|
+
}) : null;
|
|
2408
|
+
const bulkPriorityDialog = bulkTagDraft && bulkTagDraft.mode === "priority" ? h(BulkChoiceDialog, {
|
|
2409
|
+
open: true, t, title: t("bulk.priority"),
|
|
2410
|
+
description: t("bulk.dialog.summary", { count: selectedIds.size, action: t("bulk.priority") }),
|
|
2411
|
+
options: bulkPriorityOptions,
|
|
2412
|
+
confirmLabel: t("bulk.confirm.priority"),
|
|
2413
|
+
onCancel: () => setBulkTagDraft(null),
|
|
2414
|
+
onApply: onBulkPriorityApply
|
|
2415
|
+
}) : null;
|
|
2416
|
+
const bulkMoveDialog = bulkTagDraft && bulkTagDraft.mode === "move" ? h(BulkChoiceDialog, {
|
|
2417
|
+
open: true, t, title: t("bulk.move"),
|
|
2418
|
+
description: t("bulk.dialog.summary", { count: selectedIds.size, action: t("bulk.move") }),
|
|
2419
|
+
options: bulkMoveOptions,
|
|
2420
|
+
confirmLabel: t("bulk.confirm.move"),
|
|
2421
|
+
onCancel: () => setBulkTagDraft(null),
|
|
2422
|
+
onApply: onBulkMoveApply
|
|
2423
|
+
}) : null;
|
|
2424
|
+
const bulkPresetDialog = bulkPresetDraft ? h(BulkChoiceDialog, {
|
|
2425
|
+
open: true, t, title: t("bulk.presetMigrate"),
|
|
2426
|
+
description: t("bulk.dialog.summary", { count: selectedIds.size, action: t("bulk.presetMigrate") }),
|
|
2427
|
+
options: bulkPresetOptions,
|
|
2428
|
+
confirmLabel: t("bulk.confirm.presetMigrate"),
|
|
2429
|
+
onCancel: () => setBulkPresetDraft(null),
|
|
2430
|
+
onApply: onBulkPresetApply
|
|
2431
|
+
}) : null;
|
|
2432
|
+
// Bulk dialogs sit at z-index 10002 (set in CSS) so they overlap the
|
|
2433
|
+
return h(React.Fragment, null, panelModal, annotationDialog, moveDialog, deleteDialog, migrateDialog, bulkPreviewDialog, bulkProgressDialog, bulkResultDialog, bulkTagDialog, bulkPriorityDialog, bulkMoveDialog, bulkPresetDialog);
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
function HeaderAction(props) {
|
|
2437
|
+
const t = props.t;
|
|
2438
|
+
const sessionId = props.sessionId;
|
|
2439
|
+
const annotations = useAnnotations();
|
|
2440
|
+
const [annotationFor, setAnnotationFor] = useState(null);
|
|
2441
|
+
const [annotationAnchor, setAnnotationAnchor] = useState(null);
|
|
2442
|
+
// Slot framework spreads inject-factory return into top-level props,
|
|
2443
|
+
// so the action callbacks live directly on props (not under .actions).
|
|
2444
|
+
const onArchive = props.onArchive, onUnarchive = props.onUnarchive;
|
|
2445
|
+
const onMove = props.onMove, onRemove = props.onRemove;
|
|
2446
|
+
// Archive state lives in the workspace registry's archivedSessionIds
|
|
2447
|
+
// (exposed through the global standard kit's useWorkspaces selector).
|
|
2448
|
+
const useWorkspaces = props.useWorkspaces;
|
|
2449
|
+
const archivedIds = (useWorkspaces ? useWorkspaces((s) => s.archivedSessionIds) : null) || [];
|
|
2450
|
+
const isArchived = archivedIds.includes(sessionId);
|
|
2451
|
+
|
|
2452
|
+
const [confirmFor, setConfirmFor] = useState(null);
|
|
2453
|
+
const [confirmAnchor, setConfirmAnchor] = useState(null);
|
|
2454
|
+
const [moveFor, setMoveFor] = useState(null);
|
|
2455
|
+
const [moveAnchor, setMoveAnchor] = useState(null);
|
|
2456
|
+
const [workspaces, setWorkspaces] = useState([]);
|
|
2457
|
+
const runWithAlert = (label, fn) => {
|
|
2458
|
+
return Promise.resolve()
|
|
2459
|
+
.then(() => fn())
|
|
2460
|
+
.catch((e) => {
|
|
2461
|
+
try {
|
|
2462
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2463
|
+
window.alert(((t && t("error.operation")) || "操作失败:{message}").replace("{message}", msg));
|
|
2464
|
+
} catch (_) {}
|
|
2465
|
+
});
|
|
2466
|
+
};
|
|
2467
|
+
useEffect(() => {
|
|
2468
|
+
if (!moveFor) { setWorkspaces([]); return; }
|
|
2469
|
+
let alive = true;
|
|
2470
|
+
fetchApi("/workspaces").then((d) => {
|
|
2471
|
+
if (alive && d && d.ok) setWorkspaces(d.result.workspaces || []);
|
|
2472
|
+
});
|
|
2473
|
+
return () => { alive = false; };
|
|
2474
|
+
}, [moveFor ? (moveFor.id || "x") : null]);
|
|
2475
|
+
// All three HeaderAction dialogs (annotation / move / delete)
|
|
2476
|
+
// share a 360px width. Anchor the dialog's right edge to the
|
|
2477
|
+
// trigger button's right edge, with 8px screen-edge padding.
|
|
2478
|
+
const anchorFor = (el) => {
|
|
2479
|
+
if (!el || typeof el.getBoundingClientRect !== "function") return null;
|
|
2480
|
+
const r = el.getBoundingClientRect();
|
|
2481
|
+
return {
|
|
2482
|
+
top: Math.round(r.top + r.height + 6),
|
|
2483
|
+
left: Math.max(8, Math.round(r.right - 360))
|
|
2484
|
+
};
|
|
2485
|
+
};
|
|
2486
|
+
const archiveLabelKey = isArchived ? "row.unarchive" : "row.archive";
|
|
2487
|
+
return h(React.Fragment, null,
|
|
2488
|
+
annotationFor ? h(AnnotationDialog, { key: annotationFor.id, sessionId: annotationFor.id, displayTitle: annotationFor.displayTitle, t, anchor: annotationAnchor, onClose: () => { setAnnotationFor(null); setAnnotationAnchor(null); } }) : null,
|
|
2489
|
+
h("div", { className: "sm-header" },
|
|
2490
|
+
h("button", {
|
|
2491
|
+
type: "button",
|
|
2492
|
+
className: "sm-headerBtn" + (isArchived ? " sm-headerBtnActive" : ""),
|
|
2493
|
+
"aria-label": t ? t(archiveLabelKey) : (isArchived ? "移出归档" : "归档会话"),
|
|
2494
|
+
onClick: () => runWithAlert("archive", () =>
|
|
2495
|
+
isArchived ? onUnarchive(sessionId) : onArchive(sessionId)
|
|
2496
|
+
)
|
|
2497
|
+
}, isArchived ? (t ? t("row.unarchive") : "移出归档") : (t ? t("row.archive") : "归档")),
|
|
2498
|
+
|
|
2499
|
+
annotationButtons(sessionId, props.displayTitle || sessionId, t, annotations, "sm-headerBtn", patch => runWithAlert("annotations", () => annotationStore.save(sessionId, patch)), (e) => {
|
|
2500
|
+
const nextOpen = !(annotationFor && annotationFor.id === sessionId);
|
|
2501
|
+
// Do NOT clear moveFor / confirmFor here: the z-index counter
|
|
2502
|
+
// (see nextDialogZ) decides stacking, so opening the annotation
|
|
2503
|
+
// popup must not silently dismiss a previously opened move or
|
|
2504
|
+
// delete popup on the same session.
|
|
2505
|
+
if (nextOpen && e && e.currentTarget) {
|
|
2506
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
2507
|
+
setAnnotationAnchor({ top: Math.round(r.top + r.height + 6), left: Math.max(8, Math.round(r.right - 360)) });
|
|
2508
|
+
} else setAnnotationAnchor(null);
|
|
2509
|
+
setAnnotationFor(current => current?.id === sessionId ? null : { id: sessionId, displayTitle: props.displayTitle || sessionId });
|
|
2510
|
+
}, false, annotationFor?.id === sessionId),
|
|
2511
|
+
annotations.error ? h("button", { type: "button", className: "sm-headerBtn", title: t("marks.error", { message: annotations.error }), "aria-label": t("marks.retry"), onClick: () => annotationStore.load(true) }, "!") : null,
|
|
2512
|
+
|
|
2513
|
+
h("button", {
|
|
2514
|
+
type: "button",
|
|
2515
|
+
className: "sm-headerBtn" + (moveFor ? " sm-headerBtnActive" : ""),
|
|
2516
|
+
"aria-label": t ? t("row.move") : "移动至工作区",
|
|
2517
|
+
onClick: (e) => {
|
|
2518
|
+
e.stopPropagation();
|
|
2519
|
+
// Do NOT clear confirmFor / confirmAnchor here: opening the
|
|
2520
|
+
// move popup must not silently dismiss a previously opened
|
|
2521
|
+
// delete popup on the same session. The z-index counter
|
|
2522
|
+
// (see nextDialogZ) handles stacking.
|
|
2523
|
+
if (moveFor && moveFor.id === sessionId) {
|
|
2524
|
+
setMoveFor(null);
|
|
2525
|
+
setMoveAnchor(null);
|
|
2526
|
+
} else {
|
|
2527
|
+
setMoveAnchor(anchorFor(e.currentTarget));
|
|
2528
|
+
setMoveFor({ id: sessionId, displayTitle: sessionId, workspaceId: "" });
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
}, t ? t("row.move") : "移动至工作区"),
|
|
2532
|
+
h("button", {
|
|
2533
|
+
type: "button",
|
|
2534
|
+
className: "sm-headerBtn sm-headerBtnDanger" + (confirmFor ? " sm-headerBtnActive" : ""),
|
|
2535
|
+
"aria-label": t ? t("row.delete") : "删除会话",
|
|
2536
|
+
onClick: (e) => {
|
|
2537
|
+
e.stopPropagation();
|
|
2538
|
+
// Do NOT clear moveFor / moveAnchor here: opening the delete
|
|
2539
|
+
// popup must not silently dismiss a previously opened move
|
|
2540
|
+
// popup on the same session. The z-index counter (see
|
|
2541
|
+
// nextDialogZ) handles stacking.
|
|
2542
|
+
if (confirmFor && confirmFor.id === sessionId) {
|
|
2543
|
+
setConfirmFor(null);
|
|
2544
|
+
setConfirmAnchor(null);
|
|
2545
|
+
} else {
|
|
2546
|
+
setConfirmAnchor(anchorFor(e.currentTarget));
|
|
2547
|
+
setConfirmFor({ id: sessionId, displayTitle: sessionId });
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
}, t ? t("row.delete") : "删除会话")
|
|
2551
|
+
),
|
|
2552
|
+
confirmFor === null ? null : h(ConfirmDialog, {
|
|
2553
|
+
open: true,
|
|
2554
|
+
anchor: confirmAnchor,
|
|
2555
|
+
onClose: () => setConfirmFor(null),
|
|
2556
|
+
onCancel: () => setConfirmFor(null),
|
|
2557
|
+
onConfirm: () => {
|
|
2558
|
+
const target = confirmFor;
|
|
2559
|
+
setConfirmFor(null);
|
|
2560
|
+
runWithAlert("delete", () => onRemove(target.id));
|
|
2561
|
+
},
|
|
2562
|
+
title: t ? t("confirm.delete.title") : "删除会话",
|
|
2563
|
+
description: (confirmFor && (t ? t("confirm.delete.desc", { title: confirmFor.displayTitle || confirmFor.id }) : ("将删除 " + (confirmFor.displayTitle || confirmFor.id)))) || "",
|
|
2564
|
+
confirmLabel: t ? t("confirm.delete.confirm") : "确认删除",
|
|
2565
|
+
cancelLabel: t ? t("confirm.cancel") : "取消",
|
|
2566
|
+
busy: false,
|
|
2567
|
+
danger: true
|
|
2568
|
+
}),
|
|
2569
|
+
moveFor === null ? null : h(MoveDialog, {
|
|
2570
|
+
open: true,
|
|
2571
|
+
anchor: moveAnchor,
|
|
2572
|
+
t: t,
|
|
2573
|
+
workspaces: workspaces,
|
|
2574
|
+
currentWorkspaceId: moveFor.workspaceId || "",
|
|
2575
|
+
onClose: () => { setMoveFor(null); setMoveAnchor(null); },
|
|
2576
|
+
onCancel: () => { setMoveFor(null); setMoveAnchor(null); },
|
|
2577
|
+
title: t ? t("confirm.move.title") : "移动会话到工作区",
|
|
2578
|
+
description: (moveFor && (t ? t("confirm.move.desc", { title: moveFor.displayTitle || moveFor.id }) : "")) || "",
|
|
2579
|
+
sessionTitle: moveFor.displayTitle || moveFor.id,
|
|
2580
|
+
confirmLabel: t ? t("confirm.move.confirm") : "确认移动",
|
|
2581
|
+
cancelLabel: t ? t("confirm.cancel") : "取消",
|
|
2582
|
+
busy: false,
|
|
2583
|
+
onConfirm: (targetWorkspaceId) => {
|
|
2584
|
+
const target = moveFor;
|
|
2585
|
+
setMoveFor(null);
|
|
2586
|
+
setMoveAnchor(null);
|
|
2587
|
+
runWithAlert("move", () => onMove(target.id, targetWorkspaceId));
|
|
2588
|
+
}
|
|
2589
|
+
})
|
|
2590
|
+
);
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
class SafePanel extends React.Component {
|
|
2594
|
+
constructor(p) { super(p); this.state = { failed: false }; }
|
|
2595
|
+
static getDerivedStateFromError() { return { failed: true }; }
|
|
2596
|
+
componentDidCatch(error) { try { console.error("[dsh-session-manager] SessionManagerPanel crashed:", error); } catch (_) {} }
|
|
2597
|
+
render() {
|
|
2598
|
+
if (this.state.failed) return null;
|
|
2599
|
+
return h(SessionManagerPanel, {
|
|
2600
|
+
open: true,
|
|
2601
|
+
onClose: () => { try { this.props.panelStore.set(false); } catch (_) {} },
|
|
2602
|
+
useSessions: this.props.useSessions,
|
|
2603
|
+
useWorkspaces: this.props.useWorkspaces,
|
|
2604
|
+
t: this.props.t,
|
|
2605
|
+
// Bulk operations need to refresh ctx.sessions.list and the
|
|
2606
|
+
// workspace projection after /batch mutates disk state so the
|
|
2607
|
+
// panel reflects the new layout.
|
|
2608
|
+
onRefreshAfterBulk: async () => {
|
|
2609
|
+
try { await this.props.props.onBulkRefresh(); } catch (_) { /* ignore */ }
|
|
2610
|
+
},
|
|
2611
|
+
onOpen: (id) => { try { this.props.panelStore.set(false); this.props.props.onOpen(id); } catch (_) {} },
|
|
2612
|
+
onArchive: this.props.props.onArchive,
|
|
2613
|
+
onUnarchive: this.props.props.onUnarchive,
|
|
2614
|
+
onRemove: this.props.props.onRemove,
|
|
2615
|
+
onMove: this.props.props.onMove,
|
|
2616
|
+
onMigrate: this.props.props.onMigrate
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
function FooterAction(props) {
|
|
2622
|
+
const t = props.t;
|
|
2623
|
+
const onOpenPanel = props.onOpenPanel;
|
|
2624
|
+
// props.wide comes from SidebarRoot via SidebarFooterActionOwnerProps.
|
|
2625
|
+
// When the sidebar is collapsed into the 56px rail, render a 36x36
|
|
2626
|
+
// icon-only button so the Chinese label does not word-wrap vertically
|
|
2627
|
+
// (issue #16); when expanded, render a full-width row with icon +
|
|
2628
|
+
// label, left-aligned to match neighbouring footer entries.
|
|
2629
|
+
const wide = props.wide === true;
|
|
2630
|
+
const open = useSyncExternalStore(
|
|
2631
|
+
(cb) => panelStore.subscribe(cb),
|
|
2632
|
+
() => panelStore.getSnapshot(),
|
|
2633
|
+
() => panelStore.getSnapshot()
|
|
2634
|
+
);
|
|
2635
|
+
const footerClass = "sm-footer" + (wide ? " sm-footer-wide" : " sm-footer-rail");
|
|
2636
|
+
const btnClass = "sm-footerBtn" + (wide ? " sm-footerBtn-wide" : " sm-footerBtn-rail");
|
|
2637
|
+
const btnLabel = t("footer.aria");
|
|
2638
|
+
return h("div", { className: footerClass },
|
|
2639
|
+
h("button", {
|
|
2640
|
+
type: "button",
|
|
2641
|
+
className: btnClass,
|
|
2642
|
+
"aria-label": btnLabel,
|
|
2643
|
+
title: wide ? undefined : btnLabel,
|
|
2644
|
+
onClick: () => onOpenPanel()
|
|
2645
|
+
},
|
|
2646
|
+
h(P.IconArchiveOutlineRegular, { size: wide ? 14 : 18 }),
|
|
2647
|
+
wide ? h("span", { className: "sm-footerBtn-label" }, t("footer.label")) : null
|
|
2648
|
+
),
|
|
2649
|
+
open ? h(SafePanel, {
|
|
2650
|
+
panelStore: panelStore,
|
|
2651
|
+
props: props,
|
|
2652
|
+
t: t,
|
|
2653
|
+
useSessions: props.useSessions,
|
|
2654
|
+
useWorkspaces: props.useWorkspaces,
|
|
2655
|
+
}) : null
|
|
2656
|
+
);
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
function apply(ctx) {
|
|
2660
|
+
ctx.effect(() => {
|
|
2661
|
+
const refresh = () => { if (annotationStore.hasSubscribers()) void annotationStore.load(true); };
|
|
2662
|
+
window.addEventListener("focus", refresh);
|
|
2663
|
+
let channel;
|
|
2664
|
+
try {
|
|
2665
|
+
if (typeof BroadcastChannel === "function") {
|
|
2666
|
+
channel = new BroadcastChannel("dsh-session-manager-annotations-v1");
|
|
2667
|
+
channel.onmessage = event => { if (event.data?.type === "changed") refresh(); };
|
|
2668
|
+
annotationStore.notify = () => channel.postMessage({ type: "changed" });
|
|
2669
|
+
}
|
|
2670
|
+
} catch (_) { /* unavailable browser messaging: focus refresh still works */ }
|
|
2671
|
+
return () => { window.removeEventListener("focus", refresh); channel?.close(); annotationStore.notify = null; };
|
|
2672
|
+
}, "session-manager: annotation synchronization");
|
|
2673
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "session-manager: dictionaries");
|
|
2674
|
+
const actions = () => ({
|
|
2675
|
+
// DSH 0.1.6+ removed ctx.sessions.open(); the navigation owner is
|
|
2676
|
+
// `ctx.uiWorkspace.openSession()`, and the bare SessionController only
|
|
2677
|
+
// exposes `retain()`. Pick the live surface so the row's "Open" button
|
|
2678
|
+
// actually navigates on every supported host build.
|
|
2679
|
+
onOpen: (sessionId) => {
|
|
2680
|
+
const uiWorkspace = ctx.uiWorkspace;
|
|
2681
|
+
if (uiWorkspace && typeof uiWorkspace.openSession === "function") {
|
|
2682
|
+
uiWorkspace.openSession(sessionId);
|
|
2683
|
+
return;
|
|
2684
|
+
}
|
|
2685
|
+
const sessions = ctx.sessions;
|
|
2686
|
+
if (sessions && typeof sessions.retain === "function") {
|
|
2687
|
+
// The bare retain() returns a SessionReference whose release()
|
|
2688
|
+
// tears down the in-memory binding; the user normally expects the
|
|
2689
|
+
// session to stay open, so a header-less build simply keeps it.
|
|
2690
|
+
sessions.retain(sessionId, { source: "mainView" });
|
|
2691
|
+
return;
|
|
2692
|
+
}
|
|
2693
|
+
throw new Error("session-manager: no Session open API on this DSH host");
|
|
2694
|
+
},
|
|
2695
|
+
onArchive: async (sessionId) => { await ctx.workspaces.archiveSession(sessionId); },
|
|
2696
|
+
onUnarchive: async (sessionId) => {
|
|
2697
|
+
const response = await callApi("/unarchive", { sessionId });
|
|
2698
|
+
if (!response.ok) throw new Error(response.error || "unarchive failed");
|
|
2699
|
+
},
|
|
2700
|
+
onRemove: async (sessionId) => {
|
|
2701
|
+
const response = await callApi("/delete", { sessionId });
|
|
2702
|
+
if (!response.ok) throw new Error(response.error || "delete failed");
|
|
2703
|
+
await annotationStore.load(true);
|
|
2704
|
+
if (response.result?.warning) window.alert(response.result.warning);
|
|
2705
|
+
const current = ctx.sessions.list.getSnapshot().current;
|
|
2706
|
+
if (current === sessionId) ctx.sessions.clear();
|
|
2707
|
+
},
|
|
2708
|
+
onMove: async (sessionId, targetWorkspaceId) => {
|
|
2709
|
+
const wasCurrent = ctx.sessions.list.getSnapshot().current === sessionId;
|
|
2710
|
+
const response = await callApi("/move", { sessionId, targetWorkspaceId });
|
|
2711
|
+
if (!response.ok) throw new Error(response.error || "move failed");
|
|
2712
|
+
const refreshMovedSession = async () => {
|
|
2713
|
+
await Promise.allSettled([
|
|
2714
|
+
typeof ctx.workspaces?.refresh === "function" ? ctx.workspaces.refresh() : Promise.resolve(),
|
|
2715
|
+
ctx.sessions.refresh()
|
|
2716
|
+
]);
|
|
2717
|
+
if (wasCurrent && ctx.sessions.list.getSnapshot().byId[sessionId] !== undefined) {
|
|
2718
|
+
ctx.sessions.open(sessionId);
|
|
2719
|
+
}
|
|
2720
|
+
};
|
|
2721
|
+
// The 0.4.6+ move path keeps the live agent/session intact (no
|
|
2722
|
+
// session/disposed, no half-resumed Session object), so the chat
|
|
2723
|
+
// panel keeps the in-memory session as active. We still refresh
|
|
2724
|
+
// the workspace + session-list projections so the destination
|
|
2725
|
+
// workspace and the moved row appear without a manual refresh.
|
|
2726
|
+
await refreshMovedSession();
|
|
2727
|
+
setTimeout(() => { void refreshMovedSession(); }, 250);
|
|
2728
|
+
setTimeout(() => { void refreshMovedSession(); }, 900);
|
|
2729
|
+
},
|
|
2730
|
+
onMigrate: async (sessionId, toPreset) => {
|
|
2731
|
+
const wasCurrent = ctx.sessions.list.getSnapshot().current === sessionId;
|
|
2732
|
+
const response = await callApi("/preset-migrate", { sessionId, toPreset });
|
|
2733
|
+
if (!response.ok) throw new Error(response.error || "preset migration failed");
|
|
2734
|
+
// The current DSH client session store derives agentPreset from
|
|
2735
|
+
// the refreshed projection; older builds exposed noteAgentPreset,
|
|
2736
|
+
onOpenPanel: () => panelStore.toggle(),
|
|
2737
|
+
await ctx.sessions.refresh();
|
|
2738
|
+
// The migrate rewrite happens in-process on the live session, so
|
|
2739
|
+
// we just need a delayed baseline to let the api-gateway's projection
|
|
2740
|
+
// settle before re-activating the in-memory session.
|
|
2741
|
+
setTimeout(() => {
|
|
2742
|
+
void ctx.sessions.refresh().then(() => {
|
|
2743
|
+
if (wasCurrent) ctx.sessions.open(sessionId);
|
|
2744
|
+
}).catch(() => {});
|
|
2745
|
+
}, 250);
|
|
2746
|
+
},
|
|
2747
|
+
onOpenPanel: () => panelStore.toggle(),
|
|
2748
|
+
onBulkRefresh: async () => { try { await ctx.sessions.refresh(); } catch (_) { /* ignore */ } try { await ctx.workspaces.refresh(); } catch (_) { /* ignore */ } },
|
|
2749
|
+
});
|
|
2750
|
+
ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
|
|
2751
|
+
name: "conversation.session.header.actions",
|
|
2752
|
+
id: "session-manager-header",
|
|
2753
|
+
order: 40,
|
|
2754
|
+
locale: NS,
|
|
2755
|
+
inject: () => actions()
|
|
2756
|
+
}, HeaderAction));
|
|
2757
|
+
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
2758
|
+
name: "sidebar.footer.action",
|
|
2759
|
+
id: "session-manager-footer",
|
|
2760
|
+
order: 30,
|
|
2761
|
+
locale: NS,
|
|
2762
|
+
inject: () => actions()
|
|
2763
|
+
}, FooterAction));
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
const inject = ["slots", "sessions", "workspaces", "locale", "uiWorkspace"];
|
|
2767
|
+
|
|
2768
|
+
exports.apply = apply;
|
|
2769
|
+
exports.inject = inject;
|
|
2770
|
+
return module.exports;
|
|
2771
|
+
}
|
|
2772
|
+
});
|