dsh-folder-sidebar 0.17.1 → 0.18.1
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 +9 -0
- package/README.md +181 -180
- package/client.js +1837 -1766
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -1,1766 +1,1837 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure logic for dsh-folder-sidebar.
|
|
3
|
-
*
|
|
4
|
-
* This block lives at module scope, outside the Client factory, so that the
|
|
5
|
-
* exact shipped source can be exercised by `node` tests: with `window` absent
|
|
6
|
-
* the browser bootstrap below is skipped and `module.exports` exposes these
|
|
7
|
-
* functions. Nothing here touches React, the DOM, or a specific Cordis context.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const INDICATOR_PRIORITY = ['waiting', 'running', 'error', 'completed', 'stopped']
|
|
11
|
-
|
|
12
|
-
const INDICATOR_LABEL_KEY = {
|
|
13
|
-
running: 'status.running',
|
|
14
|
-
completed: 'status.completed',
|
|
15
|
-
waiting: 'status.waiting',
|
|
16
|
-
error: 'status.error',
|
|
17
|
-
stopped: 'status.stopped',
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const GLYPH_COLOR = {
|
|
21
|
-
waiting: 'var(--dsw-alias-state-warn-primary)',
|
|
22
|
-
error: 'var(--dsw-alias-state-error-primary)',
|
|
23
|
-
completed: 'var(--dsw-alias-state-success-primary)',
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const SORT_MODES = ['manual', 'name', 'recent']
|
|
27
|
-
const SORT_DIRS = ['asc', 'desc']
|
|
28
|
-
|
|
29
|
-
/** Each level keeps its own mode and direction. */
|
|
30
|
-
const SORT_KEY = 'dsh.folder-sidebar.sort'
|
|
31
|
-
|
|
32
|
-
const SORT_DEFAULT = {
|
|
33
|
-
folders: { mode: 'manual', dir: 'asc' },
|
|
34
|
-
sessions: { mode: 'recent', dir: 'desc' },
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** A mode's natural order; picking a mode resets the direction to it. */
|
|
38
|
-
const SORT_NATURAL_DIR = { manual: 'asc', name: 'asc', recent: 'desc' }
|
|
39
|
-
|
|
40
|
-
/** Archived visibility mirrors the shipped view options. */
|
|
41
|
-
const ARCHIVED_KEY = 'dsh.folder-sidebar.archived'
|
|
42
|
-
const ARCHIVED_FILTERS = ['hide', 'all', 'only']
|
|
43
|
-
|
|
44
|
-
/** Sentinel for the pseudo Workspace that owns Sessions outside every Workspace. */
|
|
45
|
-
const UNGROUPED = '__ungrouped__'
|
|
46
|
-
|
|
47
|
-
function safeStorage(storage) {
|
|
48
|
-
if (storage !== undefined && storage !== null) return storage
|
|
49
|
-
try {
|
|
50
|
-
return globalThis.localStorage
|
|
51
|
-
} catch {
|
|
52
|
-
return undefined
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Comparator sign for a direction. */
|
|
57
|
-
function directionSign(dir) {
|
|
58
|
-
return dir === 'desc' ? -1 : 1
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function normalizeSort(value, fallback) {
|
|
62
|
-
const mode = value !== null && typeof value === 'object' && SORT_MODES.includes(value.mode) ? value.mode : fallback.mode
|
|
63
|
-
const dir = value !== null && typeof value === 'object' && SORT_DIRS.includes(value.dir) ? value.dir : fallback.dir
|
|
64
|
-
return { mode, dir }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function readSortState(storage) {
|
|
68
|
-
const store = safeStorage(storage)
|
|
69
|
-
try {
|
|
70
|
-
const parsed = JSON.parse(store?.getItem(SORT_KEY) ?? 'null')
|
|
71
|
-
return {
|
|
72
|
-
folders: normalizeSort(parsed?.folders, SORT_DEFAULT.folders),
|
|
73
|
-
sessions: normalizeSort(parsed?.sessions, SORT_DEFAULT.sessions),
|
|
74
|
-
}
|
|
75
|
-
} catch {
|
|
76
|
-
return { folders: { ...SORT_DEFAULT.folders }, sessions: { ...SORT_DEFAULT.sessions } }
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function writeSortState(state, storage) {
|
|
81
|
-
try {
|
|
82
|
-
safeStorage(storage)?.setItem(SORT_KEY, JSON.stringify(state))
|
|
83
|
-
} catch {
|
|
84
|
-
// Storage may be unavailable; the choice still applies this session.
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function readArchivedFilter(storage) {
|
|
89
|
-
try {
|
|
90
|
-
const saved = safeStorage(storage)?.getItem(ARCHIVED_KEY)
|
|
91
|
-
return ARCHIVED_FILTERS.includes(saved) ? saved : 'hide'
|
|
92
|
-
} catch {
|
|
93
|
-
return 'hide'
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function writeArchivedFilter(value, storage) {
|
|
98
|
-
try {
|
|
99
|
-
safeStorage(storage)?.setItem(ARCHIVED_KEY, value)
|
|
100
|
-
} catch {
|
|
101
|
-
// Storage may be unavailable; the filter still applies this session.
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Order Workspaces for the folder list.
|
|
107
|
-
* @param items - Workspaces in Host order.
|
|
108
|
-
* @param sort - `{ mode, dir }`.
|
|
109
|
-
* @param accessors - `nameOf(item)` and `activityOf(item)`.
|
|
110
|
-
*/
|
|
111
|
-
function sortWorkspaces(items, sort, accessors) {
|
|
112
|
-
const ordered = items.slice()
|
|
113
|
-
const sign = directionSign(sort.dir)
|
|
114
|
-
if (sort.mode === 'name') {
|
|
115
|
-
ordered.sort((a, b) => accessors.nameOf(a).localeCompare(accessors.nameOf(b), undefined, { numeric: true }) * sign)
|
|
116
|
-
} else if (sort.mode === 'recent') {
|
|
117
|
-
ordered.sort((a, b) => (accessors.activityOf(a) - accessors.activityOf(b)) * sign)
|
|
118
|
-
} else if (sign < 0) {
|
|
119
|
-
ordered.reverse()
|
|
120
|
-
}
|
|
121
|
-
return ordered
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Order Sessions inside one Workspace. Pinned rows stay first in every mode;
|
|
126
|
-
* the direction applies inside a group. Descending updates are what the caller
|
|
127
|
-
* already derives, so only ascending needs a sort.
|
|
128
|
-
*/
|
|
129
|
-
function sortSessions(rows, sort, accessors) {
|
|
130
|
-
const ordered = rows.slice()
|
|
131
|
-
const sign = directionSign(sort.dir)
|
|
132
|
-
const pinOf = (row) => (accessors.isPinned(row) ? 1 : 0)
|
|
133
|
-
if (sort.mode === 'name') {
|
|
134
|
-
ordered.sort((a, b) => {
|
|
135
|
-
const pin = pinOf(b) - pinOf(a)
|
|
136
|
-
if (pin !== 0) return pin
|
|
137
|
-
return accessors.titleOf(a).localeCompare(accessors.titleOf(b), undefined, { numeric: true }) * sign
|
|
138
|
-
})
|
|
139
|
-
return ordered
|
|
140
|
-
}
|
|
141
|
-
// Both "manual" and "recent" order Sessions by recency; the direction only
|
|
142
|
-
// flips the comparison, so the result never depends on the caller's order.
|
|
143
|
-
ordered.sort((a, b) => {
|
|
144
|
-
const pin = pinOf(b) - pinOf(a)
|
|
145
|
-
if (pin !== 0) return pin
|
|
146
|
-
return ((accessors.updatedAtOf(a) ?? 0) - (accessors.updatedAtOf(b) ?? 0)) * sign
|
|
147
|
-
})
|
|
148
|
-
return ordered
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* A Workspace shows one status: highest precedence first, then the state that
|
|
153
|
-
* began earliest.
|
|
154
|
-
* @param candidates - `{ kind, since }` pairs for the Workspace's Sessions.
|
|
155
|
-
*/
|
|
156
|
-
function pickFolderKind(candidates) {
|
|
157
|
-
let best
|
|
158
|
-
for (const candidate of candidates) {
|
|
159
|
-
const priority = INDICATOR_PRIORITY.indexOf(candidate.kind)
|
|
160
|
-
if (priority < 0) continue
|
|
161
|
-
if (best === undefined || priority < best.priority || (priority === best.priority && candidate.since < best.since)) {
|
|
162
|
-
best = { kind: candidate.kind, priority, since: candidate.since }
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return best === undefined ? undefined : best.kind
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* One row's leading indicator. Green follows the client's completion-unread
|
|
170
|
-
* rule; failure comes from this bundle's projection and is not cleared by
|
|
171
|
-
* viewing. Running wins over everything, a pending question wins over activity.
|
|
172
|
-
*/
|
|
173
|
-
function indicatorOf(status, projection) {
|
|
174
|
-
const waiting = status !== undefined && status !== null
|
|
175
|
-
&& status.pendingInteraction !== undefined && status.pendingInteraction !== null
|
|
176
|
-
if (waiting) return 'waiting'
|
|
177
|
-
const running = status !== undefined && status !== null && status.running === true
|
|
178
|
-
if (running) return 'running'
|
|
179
|
-
const state = projection === undefined || projection === null ? undefined : projection.state
|
|
180
|
-
if (state === 'failed' || state === 'interrupted') return 'error'
|
|
181
|
-
if (status !== undefined && status !== null && status.completionUnread === true) return 'completed'
|
|
182
|
-
if (state === 'running') return 'running'
|
|
183
|
-
if (state === 'stopped') return 'stopped'
|
|
184
|
-
return undefined
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function relativeTime(ts, now = Date.now()) {
|
|
188
|
-
if (typeof ts !== 'number' || !Number.isFinite(ts)) return ''
|
|
189
|
-
const delta = now - ts
|
|
190
|
-
if (delta < 60_000) return '<1m'
|
|
191
|
-
if (delta < 3_600_000) return `${Math.floor(delta / 60_000)}m`
|
|
192
|
-
if (delta < 86_400_000) return `${Math.floor(delta / 3_600_000)}h`
|
|
193
|
-
if (delta < 7 * 86_400_000) return `${Math.floor(delta / 86_400_000)}d`
|
|
194
|
-
const date = new Date(ts)
|
|
195
|
-
return `${date.getMonth() + 1}/${date.getDate()}`
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function baseName(path) {
|
|
199
|
-
if (typeof path !== 'string' || path === '') return ''
|
|
200
|
-
const parts = path.split(/[\\/]+/).filter(Boolean)
|
|
201
|
-
return parts.length > 0 ? parts[parts.length - 1] : path
|
|
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
|
-
'action.
|
|
253
|
-
'action.
|
|
254
|
-
'action.
|
|
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
|
-
'action.
|
|
321
|
-
'action.
|
|
322
|
-
'action.
|
|
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
|
-
.dfs-
|
|
395
|
-
.dfs-
|
|
396
|
-
.dfs-
|
|
397
|
-
.dfs-
|
|
398
|
-
.dfs-
|
|
399
|
-
.dfs-
|
|
400
|
-
.dfs-
|
|
401
|
-
.dfs-
|
|
402
|
-
.dfs-
|
|
403
|
-
.dfs-
|
|
404
|
-
.dfs-
|
|
405
|
-
.dfs-
|
|
406
|
-
.dfs-
|
|
407
|
-
.dfs-
|
|
408
|
-
|
|
409
|
-
.dfs-
|
|
410
|
-
.dfs-
|
|
411
|
-
.dfs-
|
|
412
|
-
.dfs-
|
|
413
|
-
.dfs-
|
|
414
|
-
.dfs-
|
|
415
|
-
.dfs-
|
|
416
|
-
.dfs-
|
|
417
|
-
.dfs-
|
|
418
|
-
.dfs-
|
|
419
|
-
.dfs-
|
|
420
|
-
.dfs-
|
|
421
|
-
.dfs-
|
|
422
|
-
.dfs-
|
|
423
|
-
.dfs-
|
|
424
|
-
.dfs-
|
|
425
|
-
.dfs-
|
|
426
|
-
.dfs-
|
|
427
|
-
.dfs-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
.dfs-
|
|
431
|
-
.dfs-
|
|
432
|
-
.dfs-
|
|
433
|
-
.dfs-
|
|
434
|
-
.dfs-
|
|
435
|
-
|
|
436
|
-
.dfs-
|
|
437
|
-
.dfs-
|
|
438
|
-
.dfs-
|
|
439
|
-
.dfs-
|
|
440
|
-
.dfs-
|
|
441
|
-
.dfs-
|
|
442
|
-
.dfs-
|
|
443
|
-
.dfs-
|
|
444
|
-
.dfs-
|
|
445
|
-
.dfs-
|
|
446
|
-
.dfs-
|
|
447
|
-
.dfs-
|
|
448
|
-
.dfs-
|
|
449
|
-
|
|
450
|
-
.dfs-
|
|
451
|
-
.dfs-
|
|
452
|
-
.dfs-
|
|
453
|
-
.dfs-
|
|
454
|
-
.dfs-
|
|
455
|
-
.dfs-
|
|
456
|
-
.dfs-
|
|
457
|
-
.dfs-
|
|
458
|
-
.dfs-
|
|
459
|
-
.dfs-
|
|
460
|
-
.dfs-
|
|
461
|
-
.dfs-
|
|
462
|
-
.dfs-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
function
|
|
528
|
-
return iconFrame([
|
|
529
|
-
h('path', { key: '
|
|
530
|
-
h('
|
|
531
|
-
h('path', { key: '
|
|
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
|
-
], { size:
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
function
|
|
575
|
-
return iconFrame([
|
|
576
|
-
h('path', {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
stroke: 'currentColor',
|
|
591
|
-
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
function
|
|
596
|
-
return iconFrame([
|
|
597
|
-
h('path', {
|
|
598
|
-
key: '
|
|
599
|
-
d: '
|
|
600
|
-
|
|
601
|
-
}),
|
|
602
|
-
h('path', {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
'
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
function
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
React.
|
|
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
|
-
React.useEffect(() => {
|
|
824
|
-
if (
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
return
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
)
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
/**
|
|
1059
|
-
const
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
if (
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
if (
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
if (
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
if (
|
|
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
|
-
const
|
|
1158
|
-
if (
|
|
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
|
-
const
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
.
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
const
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
key
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
{
|
|
1701
|
-
{
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Pure logic for dsh-folder-sidebar.
|
|
3
|
+
*
|
|
4
|
+
* This block lives at module scope, outside the Client factory, so that the
|
|
5
|
+
* exact shipped source can be exercised by `node` tests: with `window` absent
|
|
6
|
+
* the browser bootstrap below is skipped and `module.exports` exposes these
|
|
7
|
+
* functions. Nothing here touches React, the DOM, or a specific Cordis context.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const INDICATOR_PRIORITY = ['waiting', 'running', 'error', 'completed', 'stopped']
|
|
11
|
+
|
|
12
|
+
const INDICATOR_LABEL_KEY = {
|
|
13
|
+
running: 'status.running',
|
|
14
|
+
completed: 'status.completed',
|
|
15
|
+
waiting: 'status.waiting',
|
|
16
|
+
error: 'status.error',
|
|
17
|
+
stopped: 'status.stopped',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const GLYPH_COLOR = {
|
|
21
|
+
waiting: 'var(--dsw-alias-state-warn-primary)',
|
|
22
|
+
error: 'var(--dsw-alias-state-error-primary)',
|
|
23
|
+
completed: 'var(--dsw-alias-state-success-primary)',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SORT_MODES = ['manual', 'name', 'recent']
|
|
27
|
+
const SORT_DIRS = ['asc', 'desc']
|
|
28
|
+
|
|
29
|
+
/** Each level keeps its own mode and direction. */
|
|
30
|
+
const SORT_KEY = 'dsh.folder-sidebar.sort'
|
|
31
|
+
|
|
32
|
+
const SORT_DEFAULT = {
|
|
33
|
+
folders: { mode: 'manual', dir: 'asc' },
|
|
34
|
+
sessions: { mode: 'recent', dir: 'desc' },
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A mode's natural order; picking a mode resets the direction to it. */
|
|
38
|
+
const SORT_NATURAL_DIR = { manual: 'asc', name: 'asc', recent: 'desc' }
|
|
39
|
+
|
|
40
|
+
/** Archived visibility mirrors the shipped view options. */
|
|
41
|
+
const ARCHIVED_KEY = 'dsh.folder-sidebar.archived'
|
|
42
|
+
const ARCHIVED_FILTERS = ['hide', 'all', 'only']
|
|
43
|
+
|
|
44
|
+
/** Sentinel for the pseudo Workspace that owns Sessions outside every Workspace. */
|
|
45
|
+
const UNGROUPED = '__ungrouped__'
|
|
46
|
+
|
|
47
|
+
function safeStorage(storage) {
|
|
48
|
+
if (storage !== undefined && storage !== null) return storage
|
|
49
|
+
try {
|
|
50
|
+
return globalThis.localStorage
|
|
51
|
+
} catch {
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Comparator sign for a direction. */
|
|
57
|
+
function directionSign(dir) {
|
|
58
|
+
return dir === 'desc' ? -1 : 1
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeSort(value, fallback) {
|
|
62
|
+
const mode = value !== null && typeof value === 'object' && SORT_MODES.includes(value.mode) ? value.mode : fallback.mode
|
|
63
|
+
const dir = value !== null && typeof value === 'object' && SORT_DIRS.includes(value.dir) ? value.dir : fallback.dir
|
|
64
|
+
return { mode, dir }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readSortState(storage) {
|
|
68
|
+
const store = safeStorage(storage)
|
|
69
|
+
try {
|
|
70
|
+
const parsed = JSON.parse(store?.getItem(SORT_KEY) ?? 'null')
|
|
71
|
+
return {
|
|
72
|
+
folders: normalizeSort(parsed?.folders, SORT_DEFAULT.folders),
|
|
73
|
+
sessions: normalizeSort(parsed?.sessions, SORT_DEFAULT.sessions),
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
return { folders: { ...SORT_DEFAULT.folders }, sessions: { ...SORT_DEFAULT.sessions } }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function writeSortState(state, storage) {
|
|
81
|
+
try {
|
|
82
|
+
safeStorage(storage)?.setItem(SORT_KEY, JSON.stringify(state))
|
|
83
|
+
} catch {
|
|
84
|
+
// Storage may be unavailable; the choice still applies this session.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readArchivedFilter(storage) {
|
|
89
|
+
try {
|
|
90
|
+
const saved = safeStorage(storage)?.getItem(ARCHIVED_KEY)
|
|
91
|
+
return ARCHIVED_FILTERS.includes(saved) ? saved : 'hide'
|
|
92
|
+
} catch {
|
|
93
|
+
return 'hide'
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function writeArchivedFilter(value, storage) {
|
|
98
|
+
try {
|
|
99
|
+
safeStorage(storage)?.setItem(ARCHIVED_KEY, value)
|
|
100
|
+
} catch {
|
|
101
|
+
// Storage may be unavailable; the filter still applies this session.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Order Workspaces for the folder list.
|
|
107
|
+
* @param items - Workspaces in Host order.
|
|
108
|
+
* @param sort - `{ mode, dir }`.
|
|
109
|
+
* @param accessors - `nameOf(item)` and `activityOf(item)`.
|
|
110
|
+
*/
|
|
111
|
+
function sortWorkspaces(items, sort, accessors) {
|
|
112
|
+
const ordered = items.slice()
|
|
113
|
+
const sign = directionSign(sort.dir)
|
|
114
|
+
if (sort.mode === 'name') {
|
|
115
|
+
ordered.sort((a, b) => accessors.nameOf(a).localeCompare(accessors.nameOf(b), undefined, { numeric: true }) * sign)
|
|
116
|
+
} else if (sort.mode === 'recent') {
|
|
117
|
+
ordered.sort((a, b) => (accessors.activityOf(a) - accessors.activityOf(b)) * sign)
|
|
118
|
+
} else if (sign < 0) {
|
|
119
|
+
ordered.reverse()
|
|
120
|
+
}
|
|
121
|
+
return ordered
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Order Sessions inside one Workspace. Pinned rows stay first in every mode;
|
|
126
|
+
* the direction applies inside a group. Descending updates are what the caller
|
|
127
|
+
* already derives, so only ascending needs a sort.
|
|
128
|
+
*/
|
|
129
|
+
function sortSessions(rows, sort, accessors) {
|
|
130
|
+
const ordered = rows.slice()
|
|
131
|
+
const sign = directionSign(sort.dir)
|
|
132
|
+
const pinOf = (row) => (accessors.isPinned(row) ? 1 : 0)
|
|
133
|
+
if (sort.mode === 'name') {
|
|
134
|
+
ordered.sort((a, b) => {
|
|
135
|
+
const pin = pinOf(b) - pinOf(a)
|
|
136
|
+
if (pin !== 0) return pin
|
|
137
|
+
return accessors.titleOf(a).localeCompare(accessors.titleOf(b), undefined, { numeric: true }) * sign
|
|
138
|
+
})
|
|
139
|
+
return ordered
|
|
140
|
+
}
|
|
141
|
+
// Both "manual" and "recent" order Sessions by recency; the direction only
|
|
142
|
+
// flips the comparison, so the result never depends on the caller's order.
|
|
143
|
+
ordered.sort((a, b) => {
|
|
144
|
+
const pin = pinOf(b) - pinOf(a)
|
|
145
|
+
if (pin !== 0) return pin
|
|
146
|
+
return ((accessors.updatedAtOf(a) ?? 0) - (accessors.updatedAtOf(b) ?? 0)) * sign
|
|
147
|
+
})
|
|
148
|
+
return ordered
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A Workspace shows one status: highest precedence first, then the state that
|
|
153
|
+
* began earliest.
|
|
154
|
+
* @param candidates - `{ kind, since }` pairs for the Workspace's Sessions.
|
|
155
|
+
*/
|
|
156
|
+
function pickFolderKind(candidates) {
|
|
157
|
+
let best
|
|
158
|
+
for (const candidate of candidates) {
|
|
159
|
+
const priority = INDICATOR_PRIORITY.indexOf(candidate.kind)
|
|
160
|
+
if (priority < 0) continue
|
|
161
|
+
if (best === undefined || priority < best.priority || (priority === best.priority && candidate.since < best.since)) {
|
|
162
|
+
best = { kind: candidate.kind, priority, since: candidate.since }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return best === undefined ? undefined : best.kind
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* One row's leading indicator. Green follows the client's completion-unread
|
|
170
|
+
* rule; failure comes from this bundle's projection and is not cleared by
|
|
171
|
+
* viewing. Running wins over everything, a pending question wins over activity.
|
|
172
|
+
*/
|
|
173
|
+
function indicatorOf(status, projection) {
|
|
174
|
+
const waiting = status !== undefined && status !== null
|
|
175
|
+
&& status.pendingInteraction !== undefined && status.pendingInteraction !== null
|
|
176
|
+
if (waiting) return 'waiting'
|
|
177
|
+
const running = status !== undefined && status !== null && status.running === true
|
|
178
|
+
if (running) return 'running'
|
|
179
|
+
const state = projection === undefined || projection === null ? undefined : projection.state
|
|
180
|
+
if (state === 'failed' || state === 'interrupted') return 'error'
|
|
181
|
+
if (status !== undefined && status !== null && status.completionUnread === true) return 'completed'
|
|
182
|
+
if (state === 'running') return 'running'
|
|
183
|
+
if (state === 'stopped') return 'stopped'
|
|
184
|
+
return undefined
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function relativeTime(ts, now = Date.now()) {
|
|
188
|
+
if (typeof ts !== 'number' || !Number.isFinite(ts)) return ''
|
|
189
|
+
const delta = now - ts
|
|
190
|
+
if (delta < 60_000) return '<1m'
|
|
191
|
+
if (delta < 3_600_000) return `${Math.floor(delta / 60_000)}m`
|
|
192
|
+
if (delta < 86_400_000) return `${Math.floor(delta / 3_600_000)}h`
|
|
193
|
+
if (delta < 7 * 86_400_000) return `${Math.floor(delta / 86_400_000)}d`
|
|
194
|
+
const date = new Date(ts)
|
|
195
|
+
return `${date.getMonth() + 1}/${date.getDate()}`
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function baseName(path) {
|
|
199
|
+
if (typeof path !== 'string' || path === '') return ''
|
|
200
|
+
const parts = path.split(/[\\/]+/).filter(Boolean)
|
|
201
|
+
return parts.length > 0 ? parts[parts.length - 1] : path
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* How far a title must travel to show its tail, or 0 when it already fits.
|
|
206
|
+
* A couple of pixels of rounding should not start a marquee.
|
|
207
|
+
*/
|
|
208
|
+
function marqueeShiftOf(scrollWidth, clientWidth) {
|
|
209
|
+
if (!Number.isFinite(scrollWidth) || !Number.isFinite(clientWidth)) return 0
|
|
210
|
+
const shift = Math.round(scrollWidth - clientWidth)
|
|
211
|
+
return shift > 2 ? shift : 0
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Longer titles scroll slower, so the reading speed stays roughly even. */
|
|
215
|
+
function marqueeDurationOf(shift) {
|
|
216
|
+
return Math.min(14000, Math.max(2200, Math.round(shift * 26)))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Web Client half of dsh-folder-sidebar.
|
|
221
|
+
*
|
|
222
|
+
* Replaces the sidebar's `sidebar.workspaces` region (shipped browser:
|
|
223
|
+
* ui-workspace) with a drill-down "folder" view:
|
|
224
|
+
* 1. folder list — one row per Workspace; the folder icon itself is that
|
|
225
|
+
* Workspace's single status cell, showing its oldest Session state;
|
|
226
|
+
* 2. enter a folder — only that Workspace's Sessions, each with its own
|
|
227
|
+
* state colour, a "…" menu, inline rename, and a back row.
|
|
228
|
+
* A search field switches the region to a flat result list across Workspaces.
|
|
229
|
+
*
|
|
230
|
+
* Data comes from the slot's standard projection props (`useWorkspaces`,
|
|
231
|
+
* `useSessions`, `useSessionStatus`); actions go through Client services
|
|
232
|
+
* (`uiWorkspace`, `workspaces`, `sessions`). Failure state is read from this
|
|
233
|
+
* bundle's Host projection (`folder-sidebar/turn`), because a Session summary
|
|
234
|
+
* carries no failure field.
|
|
235
|
+
*/
|
|
236
|
+
|
|
237
|
+
if (typeof window !== 'undefined' && window.__ModuleLoader__ !== undefined) {
|
|
238
|
+
window.__ModuleLoader__.load({
|
|
239
|
+
id: 'dsh-folder-sidebar',
|
|
240
|
+
factory(require) {
|
|
241
|
+
const React = require('react')
|
|
242
|
+
const h = React.createElement
|
|
243
|
+
|
|
244
|
+
const NS = 'folder-sidebar'
|
|
245
|
+
|
|
246
|
+
/** Host projection key: this bundle's per-Session last-turn state. */
|
|
247
|
+
const TURN_KEY = 'folder-sidebar/turn'
|
|
248
|
+
|
|
249
|
+
const DICTS = {
|
|
250
|
+
zh: {
|
|
251
|
+
'section.title': '工作区',
|
|
252
|
+
'action.back': '全部工作区',
|
|
253
|
+
'action.newSession': '新会话',
|
|
254
|
+
'action.addWorkspace': '添加工作区',
|
|
255
|
+
'action.search': '搜索会话',
|
|
256
|
+
'action.close': '关闭',
|
|
257
|
+
'action.menu': '更多操作',
|
|
258
|
+
'action.pin': '置顶',
|
|
259
|
+
'action.unpin': '取消置顶',
|
|
260
|
+
'action.rename': '重命名',
|
|
261
|
+
'action.archive': '归档',
|
|
262
|
+
'action.fork': '分叉',
|
|
263
|
+
'action.copyId': '复制会话 ID',
|
|
264
|
+
'action.renameWorkspace': '重命名工作区',
|
|
265
|
+
'action.deleteWorkspace': '删除工作区',
|
|
266
|
+
'action.newSessionHere': '在此新建会话',
|
|
267
|
+
'action.moveUp': '上移',
|
|
268
|
+
'action.moveDown': '下移',
|
|
269
|
+
'action.sort': '排序',
|
|
270
|
+
'level.folders': '文件夹',
|
|
271
|
+
'level.sessions': '会话',
|
|
272
|
+
'sort.manual': '手动顺序',
|
|
273
|
+
'sort.name': '按名称',
|
|
274
|
+
'sort.recent': '按最近活动',
|
|
275
|
+
'sort.asc': '正序',
|
|
276
|
+
'sort.desc': '倒序',
|
|
277
|
+
'confirm.archive': '该会话仍在运行,停止并归档?',
|
|
278
|
+
'confirm.deleteWorkspace': '删除这个工作区?会话与文件不会被删除。',
|
|
279
|
+
'confirm.yes': '确认',
|
|
280
|
+
'confirm.cancel': '取消',
|
|
281
|
+
'empty.workspaces': '还没有工作区',
|
|
282
|
+
'empty.sessions': '这个工作区还没有会话',
|
|
283
|
+
'empty.search': '没有匹配的会话',
|
|
284
|
+
'empty.archived': '暂无已归档会话',
|
|
285
|
+
'group.ungrouped': '未分组',
|
|
286
|
+
'action.undo': '撤销',
|
|
287
|
+
'action.unarchive': '取消归档',
|
|
288
|
+
'action.view': '视图选项',
|
|
289
|
+
'view.hide': '隐藏已归档',
|
|
290
|
+
'view.all': '全部会话',
|
|
291
|
+
'view.only': '仅显示已归档',
|
|
292
|
+
'notice.archived': '已归档',
|
|
293
|
+
'notice.archivedNotOpenable': '已归档,取消归档后才能打开',
|
|
294
|
+
'notice.pinFailed': '置顶操作失败',
|
|
295
|
+
'notice.archiveFailed': '归档失败',
|
|
296
|
+
'notice.unarchiveFailed': '取消归档失败',
|
|
297
|
+
'notice.renameFailed': '重命名失败',
|
|
298
|
+
'notice.forkFailed': '分叉失败',
|
|
299
|
+
'notice.moveFailed': '调整顺序失败',
|
|
300
|
+
'notice.deleteFailed': '删除工作区失败',
|
|
301
|
+
'notice.addWorkspaceFailed': '添加工作区失败',
|
|
302
|
+
'session.untitled': '未命名会话',
|
|
303
|
+
'copied': '已复制',
|
|
304
|
+
'unit.sessions': '个会话',
|
|
305
|
+
'rail.expand': '展开侧栏',
|
|
306
|
+
'search.placeholder': '搜索会话…',
|
|
307
|
+
'search.content': '内容匹配',
|
|
308
|
+
'status.running': '运行中',
|
|
309
|
+
'status.completed': '已完成',
|
|
310
|
+
'status.error': '失败或中断',
|
|
311
|
+
'status.waiting': '等待你的回答',
|
|
312
|
+
'status.waitingApproval': '等待审批',
|
|
313
|
+
'status.waitingPlanReview': '等待计划确认',
|
|
314
|
+
'status.waitingAnswer': '等待回答',
|
|
315
|
+
'status.stopped': '已停止',
|
|
316
|
+
'status.idle': '空闲',
|
|
317
|
+
},
|
|
318
|
+
en: {
|
|
319
|
+
'section.title': 'Workspaces',
|
|
320
|
+
'action.back': 'All workspaces',
|
|
321
|
+
'action.newSession': 'New session',
|
|
322
|
+
'action.addWorkspace': 'Add workspace',
|
|
323
|
+
'action.search': 'Search sessions',
|
|
324
|
+
'action.close': 'Close',
|
|
325
|
+
'action.menu': 'More actions',
|
|
326
|
+
'action.pin': 'Pin',
|
|
327
|
+
'action.unpin': 'Unpin',
|
|
328
|
+
'action.rename': 'Rename',
|
|
329
|
+
'action.archive': 'Archive',
|
|
330
|
+
'action.fork': 'Fork',
|
|
331
|
+
'action.copyId': 'Copy session ID',
|
|
332
|
+
'action.renameWorkspace': 'Rename workspace',
|
|
333
|
+
'action.deleteWorkspace': 'Delete workspace',
|
|
334
|
+
'action.newSessionHere': 'New session here',
|
|
335
|
+
'action.moveUp': 'Move up',
|
|
336
|
+
'action.moveDown': 'Move down',
|
|
337
|
+
'action.sort': 'Sort',
|
|
338
|
+
'level.folders': 'folders',
|
|
339
|
+
'level.sessions': 'sessions',
|
|
340
|
+
'sort.manual': 'Manual order',
|
|
341
|
+
'sort.name': 'By name',
|
|
342
|
+
'sort.recent': 'By recent activity',
|
|
343
|
+
'sort.asc': 'Ascending',
|
|
344
|
+
'sort.desc': 'Descending',
|
|
345
|
+
'confirm.archive': 'This session is still running. Stop it and archive?',
|
|
346
|
+
'confirm.deleteWorkspace': 'Delete this workspace? Sessions and files are kept.',
|
|
347
|
+
'confirm.yes': 'Confirm',
|
|
348
|
+
'confirm.cancel': 'Cancel',
|
|
349
|
+
'empty.workspaces': 'No workspaces yet',
|
|
350
|
+
'empty.sessions': 'No sessions in this workspace yet',
|
|
351
|
+
'empty.search': 'No matching sessions',
|
|
352
|
+
'empty.archived': 'No archived sessions',
|
|
353
|
+
'group.ungrouped': 'Ungrouped',
|
|
354
|
+
'action.undo': 'Undo',
|
|
355
|
+
'action.unarchive': 'Unarchive',
|
|
356
|
+
'action.view': 'View options',
|
|
357
|
+
'view.hide': 'Hide archived',
|
|
358
|
+
'view.all': 'All sessions',
|
|
359
|
+
'view.only': 'Archived only',
|
|
360
|
+
'notice.archived': 'Archived',
|
|
361
|
+
'notice.archivedNotOpenable': 'Archived — unarchive it to open',
|
|
362
|
+
'notice.pinFailed': 'Pin failed',
|
|
363
|
+
'notice.archiveFailed': 'Archive failed',
|
|
364
|
+
'notice.unarchiveFailed': 'Unarchive failed',
|
|
365
|
+
'notice.renameFailed': 'Rename failed',
|
|
366
|
+
'notice.forkFailed': 'Fork failed',
|
|
367
|
+
'notice.moveFailed': 'Reorder failed',
|
|
368
|
+
'notice.deleteFailed': 'Deleting the workspace failed',
|
|
369
|
+
'notice.addWorkspaceFailed': 'Adding the workspace failed',
|
|
370
|
+
'session.untitled': 'Untitled session',
|
|
371
|
+
'copied': 'Copied',
|
|
372
|
+
'unit.sessions': 'sessions',
|
|
373
|
+
'rail.expand': 'Expand sidebar',
|
|
374
|
+
'search.placeholder': 'Search sessions…',
|
|
375
|
+
'search.content': 'content match',
|
|
376
|
+
'status.running': 'Running',
|
|
377
|
+
'status.completed': 'Completed',
|
|
378
|
+
'status.error': 'Failed or interrupted',
|
|
379
|
+
'status.waiting': 'Waiting for your answer',
|
|
380
|
+
'status.waitingApproval': 'Waiting for approval',
|
|
381
|
+
'status.waitingPlanReview': 'Waiting for plan review',
|
|
382
|
+
'status.waitingAnswer': 'Waiting for your answer',
|
|
383
|
+
'status.stopped': 'Stopped',
|
|
384
|
+
'status.idle': 'Idle',
|
|
385
|
+
},
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Precedence for a Workspace's single status cell, matching the Session row
|
|
390
|
+
* order: an actionable question outranks activity, a failure outranks an
|
|
391
|
+
* unread result. Ties inside one kind fall back to the oldest state.
|
|
392
|
+
*/
|
|
393
|
+
const CSS = `
|
|
394
|
+
.dfs-root{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;height:100%;font-size:13px;color:var(--dsw-alias-label-primary);padding:4px 8px 10px}
|
|
395
|
+
.dfs-head{display:flex;align-items:center;gap:4px;flex:none;box-sizing:border-box;height:32px;margin:0 2px 8px;padding:0 2px}
|
|
396
|
+
.dfs-topbar{position:relative;display:flex;align-items:center;flex:none;box-sizing:border-box;height:32px;margin:0 2px 8px}
|
|
397
|
+
.dfs-topbar > .dfs-head{flex:1 1 auto;min-width:0;height:100%;margin:0;padding:0 2px;opacity:1;visibility:visible;transition:opacity .15s ease,visibility 0s linear 0s}
|
|
398
|
+
.dfs-topbar > .dfs-search{position:absolute;inset:0;height:100%;margin:0;opacity:0;visibility:hidden;pointer-events:none;transform:scaleX(.94);transform-origin:right center;transition:opacity .15s ease,transform .18s ease,visibility 0s linear .18s}
|
|
399
|
+
.dfs-topbar.is-search > .dfs-head{opacity:0;pointer-events:none;visibility:hidden;transition:opacity .15s ease,visibility 0s linear .15s}
|
|
400
|
+
.dfs-topbar.is-search > .dfs-search{opacity:1;visibility:visible;pointer-events:auto;transform:none;transition:opacity .15s ease,transform .18s ease,visibility 0s linear 0s}
|
|
401
|
+
@media (prefers-reduced-motion: reduce){.dfs-topbar > .dfs-head,.dfs-topbar > .dfs-search{transition:none}}
|
|
402
|
+
.dfs-headTitle{flex:1;min-width:0;font-size:12px;font-weight:500;letter-spacing:.02em;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
403
|
+
.dfs-headBack{width:20px;height:20px;font-size:14px;color:var(--dsw-alias-label-secondary)}
|
|
404
|
+
.dfs-iconBtn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;flex:none}
|
|
405
|
+
.dfs-iconBtn:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
|
|
406
|
+
.dfs-iconBtn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
407
|
+
.dfs-search{display:flex;align-items:center;flex:none;gap:8px;box-sizing:border-box;height:32px;margin:0 2px 8px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-base);transition:border-color .12s ease}
|
|
408
|
+
|
|
409
|
+
.dfs-search:focus-within{border-color:var(--dsw-alias-brand-primary)}
|
|
410
|
+
.dfs-search input{flex:1;min-width:0;padding:0;border:0;outline:0;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:18px}
|
|
411
|
+
.dfs-search input::placeholder{color:var(--dsw-alias-label-secondary);opacity:.75}
|
|
412
|
+
.dfs-crumb{display:flex;align-items:center;flex:none;gap:8px;box-sizing:border-box;height:28px;margin:0 2px 2px;padding:0 8px;border-radius:7px;cursor:pointer}
|
|
413
|
+
.dfs-crumb:hover{background:var(--dsw-alias-bg-layer-2)}
|
|
414
|
+
.dfs-crumb:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
415
|
+
.dfs-crumbChevron{flex:none;width:12px;text-align:center;font-size:13px;color:var(--dsw-alias-label-secondary)}
|
|
416
|
+
.dfs-crumb .dfs-folderGlyph{display:inline-flex;align-items:center}
|
|
417
|
+
.dfs-crumb .dfs-iconBtn{width:20px;height:20px}
|
|
418
|
+
.dfs-crumbName{flex:1;min-width:0;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
419
|
+
.dfs-list{display:flex;flex-direction:column;flex:1 1 auto;gap:2px;overflow-y:auto;min-height:0;padding-bottom:2px}
|
|
420
|
+
.dfs-rowWrap{position:relative;display:flex;flex-direction:column}
|
|
421
|
+
.dfs-rowWrap.is-drop-before:before,.dfs-rowWrap.is-drop-after:after{content:"";position:absolute;left:4px;right:4px;height:2px;border-radius:1px;background:var(--dsw-alias-brand-primary);z-index:5;pointer-events:none}
|
|
422
|
+
.dfs-rowWrap.is-drop-before:before{top:-1px}
|
|
423
|
+
.dfs-rowWrap.is-drop-after:after{bottom:-1px}
|
|
424
|
+
.dfs-rowWrap[draggable=true]{cursor:grab}
|
|
425
|
+
.dfs-row{display:flex;align-items:center;gap:8px;width:100%;box-sizing:border-box;padding:5px 8px;border:0;border-radius:7px;background:transparent;color:inherit;font:inherit;line-height:1.3;text-align:left;cursor:pointer}
|
|
426
|
+
.dfs-row:hover{background:var(--dsw-alias-bg-layer-2)}
|
|
427
|
+
.dfs-row:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
428
|
+
.dfs-row.is-current{background:var(--dsw-alias-bg-layer-1)}
|
|
429
|
+
.dfs-row.is-current .dfs-title{font-weight:600}
|
|
430
|
+
.dfs-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
431
|
+
.dfs-titleText{display:inline-block;white-space:nowrap;will-change:transform}
|
|
432
|
+
.dfs-title.is-marquee{text-overflow:clip}
|
|
433
|
+
.dfs-title.is-marquee .dfs-titleText,
|
|
434
|
+
.dfs-crumbName.is-marquee .dfs-titleText{animation:dfs-title-marquee var(--dfs-marquee-duration,4s) ease-in-out infinite alternate}
|
|
435
|
+
@keyframes dfs-title-marquee{0%,10%{transform:translateX(0)}90%,100%{transform:translateX(var(--dfs-marquee-shift,0))}}
|
|
436
|
+
@media (prefers-reduced-motion: reduce){.dfs-title.is-marquee .dfs-titleText,.dfs-crumbName.is-marquee .dfs-titleText{animation:none}}
|
|
437
|
+
.dfs-snippet{flex:1 1 100%;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;color:var(--dsw-alias-label-secondary)}
|
|
438
|
+
.dfs-time{flex:none;font-size:11px;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary)}
|
|
439
|
+
.dfs-status{display:inline-flex;align-items:center;justify-content:center;flex:none;width:18px;height:18px}
|
|
440
|
+
.dfs-statusDot{position:relative;width:12px;height:12px;border-radius:50%;color:var(--dsw-alias-state-idle-primary)}
|
|
441
|
+
.dfs-statusDot:after{content:"";position:absolute;inset:20%;border-radius:50%;background:currentColor}
|
|
442
|
+
.dfs-statusDot.is-none{color:var(--dsw-alias-state-idle-primary);opacity:.55}
|
|
443
|
+
.dfs-statusDot.is-completed{color:var(--dsw-alias-state-success-primary)}
|
|
444
|
+
.dfs-statusDot.is-waiting{color:var(--dsw-alias-state-warn-primary)}
|
|
445
|
+
.dfs-statusDot.is-error{color:var(--dsw-alias-state-error-primary)}
|
|
446
|
+
.dfs-statusDot.is-stopped{color:var(--dsw-alias-state-idle-primary)}
|
|
447
|
+
.dfs-statusDot.is-stopped:after{background:transparent;inset:15%;border:1.5px solid currentColor}
|
|
448
|
+
.dfs-spinner{width:14px;height:14px;border-radius:50%;border:1.5px solid var(--dsw-alias-border-l2);border-top-color:var(--dsw-alias-label-secondary);animation:dfs-spin .8s linear infinite}
|
|
449
|
+
@keyframes dfs-spin{to{transform:rotate(360deg)}}
|
|
450
|
+
@media (prefers-reduced-motion: reduce){.dfs-spinner{animation:none;border-top-color:var(--dsw-alias-label-secondary);opacity:.85}}
|
|
451
|
+
.dfs-statusFolder{width:18px}
|
|
452
|
+
.dfs-glyph{font-size:15px;font-weight:700;line-height:1}
|
|
453
|
+
.dfs-folderGlyph{display:inline-flex;align-items:center;justify-content:center}
|
|
454
|
+
.dfs-badge{flex:none;min-width:16px;text-align:center;font-size:11px;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary)}
|
|
455
|
+
.dfs-rowActions{display:inline-flex;align-items:center;gap:2px;flex:none;opacity:0;transition:opacity .12s ease}
|
|
456
|
+
.dfs-rowWrap:hover .dfs-rowActions,.dfs-rowActions.is-open{opacity:1}
|
|
457
|
+
.dfs-pin{flex:none;font-size:10px;color:var(--dsw-alias-brand-primary)}
|
|
458
|
+
.dfs-input{flex:1;min-width:0;height:20px;padding:0 4px;border:1px solid var(--dsw-alias-border-l2);border-radius:4px;background:var(--dsw-alias-bg-overlay);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px}
|
|
459
|
+
.dfs-menu{position:fixed;z-index:40;width:168px;padding:4px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-overlay);box-shadow:0 8px 24px rgba(0,0,0,.18);display:flex;flex-direction:column;gap:1px}
|
|
460
|
+
.dfs-menuItem{display:flex;align-items:center;gap:4px;width:100%;box-sizing:border-box;padding:5px 8px;border:0;border-radius:5px;background:transparent;color:inherit;font:inherit;font-size:12px;text-align:left;cursor:pointer}
|
|
461
|
+
.dfs-menuCheck{flex:none;width:16px;text-align:center;color:var(--dsw-alias-brand-primary)}
|
|
462
|
+
.dfs-menuLabel{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
463
|
+
.dfs-menuItem:hover{background:var(--dsw-alias-bg-layer-2)}
|
|
464
|
+
.dfs-menuItem.is-danger{color:var(--dsw-alias-state-error-primary)}
|
|
465
|
+
.dfs-menuDivider{height:1px;margin:4px 2px;background:var(--dsw-alias-border-l1)}
|
|
466
|
+
.dfs-menuCaption{padding:3px 8px 1px;font-size:11px;line-height:1.4;color:var(--dsw-alias-label-secondary);opacity:.8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
467
|
+
.dfs-confirm{display:flex;align-items:center;gap:6px;padding:5px 8px;font-size:11px;color:var(--dsw-alias-label-secondary)}
|
|
468
|
+
.dfs-confirmText{flex:1;min-width:0;line-height:1.4}
|
|
469
|
+
.dfs-miniBtn{padding:2px 7px;border:1px solid var(--dsw-alias-border-l1);border-radius:5px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:11px;cursor:pointer}
|
|
470
|
+
.dfs-miniBtn.is-danger{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
|
|
471
|
+
.dfs-miniBtn:hover{background:var(--dsw-alias-bg-layer-2)}
|
|
472
|
+
.dfs-empty{padding:10px 8px;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary)}
|
|
473
|
+
.dfs-row.is-archived{opacity:.55}
|
|
474
|
+
.dfs-notice{display:flex;align-items:center;gap:8px;margin:6px 2px 0;padding:6px 10px;border:1px solid var(--dsw-alias-border-l1);border-radius:7px;background:var(--dsw-alias-bg-layer-2);font-size:12px;line-height:1.4}
|
|
475
|
+
.dfs-noticeText{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary)}
|
|
476
|
+
.dfs-noticeAction{flex:none;padding:0 2px;border:0;background:transparent;color:var(--dsw-alias-brand-primary);font:inherit;font-size:12px;cursor:pointer}
|
|
477
|
+
.dfs-noticeAction:hover{text-decoration:underline}
|
|
478
|
+
.dfs-hover{position:fixed;z-index:39;min-width:150px;max-width:260px;padding:6px 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-overlay);box-shadow:0 8px 24px rgba(0,0,0,.18);display:flex;flex-direction:column;gap:2px;pointer-events:none}
|
|
479
|
+
.dfs-hoverRow{display:flex;align-items:center;gap:6px;font-size:12px;line-height:1.4;color:var(--dsw-alias-label-secondary)}
|
|
480
|
+
.dfs-hoverRow .dfs-status{width:18px;height:16px}
|
|
481
|
+
.dfs-hoverLabel{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
482
|
+
.dfs-hoverSpacer{width:16px;flex:none}
|
|
483
|
+
.dfs-rail{display:flex;flex-direction:column;align-items:center;padding-top:8px}
|
|
484
|
+
`
|
|
485
|
+
|
|
486
|
+
// Icons below reproduce the shipped `ui-primitives` geometry (16×16 grid,
|
|
487
|
+
// stroke width 1 for the "Regular" weight) instead of importing the package.
|
|
488
|
+
function iconFrame(children, options) {
|
|
489
|
+
const size = options?.size ?? 16
|
|
490
|
+
return h('svg', {
|
|
491
|
+
width: size, height: size, viewBox: '0 0 16 16', fill: 'none',
|
|
492
|
+
xmlns: 'http://www.w3.org/2000/svg', 'aria-hidden': true,
|
|
493
|
+
strokeWidth: 1,
|
|
494
|
+
style: { flex: 'none', color: options?.color ?? 'currentColor' },
|
|
495
|
+
}, children)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** IconFolderCloseRegular */
|
|
499
|
+
function FolderIcon(options) {
|
|
500
|
+
return iconFrame([
|
|
501
|
+
h('path', {
|
|
502
|
+
key: 'body',
|
|
503
|
+
d: 'M1.50439 3.11059C1.50439 2.55831 1.95211 2.1106 2.50439 2.1106H5.43389C5.67773 2.1106 5.91318 2.19969 6.09593 2.36113L7.71649 3.79265C7.89924 3.95409 8.1347 4.04319 8.3785 4.04319H13.4958C14.0481 4.04319 14.4958 4.4909 14.4958 5.04319V12.8894C14.4958 13.4417 14.0481 13.8894 13.4958 13.8894H2.50439C1.95211 13.8894 1.50439 13.4417 1.50439 12.8894V4.04319V3.11059Z',
|
|
504
|
+
stroke: 'currentColor',
|
|
505
|
+
}),
|
|
506
|
+
h('path', { key: 'line', d: 'M3.63501 7.66614H12.3647', stroke: 'currentColor' }),
|
|
507
|
+
], { size: options?.size ?? 16, color: options?.color })
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** IconTriangleRightFillRegular — the shipped row arrow. */
|
|
511
|
+
function ChevronIcon() {
|
|
512
|
+
return iconFrame(h('path', {
|
|
513
|
+
d: 'M5.5 4.5C5.5 4.40714 5.52586 4.31612 5.57467 4.23713C5.62349 4.15815 5.69334 4.09431 5.77639 4.05279C5.85945 4.01126 5.95242 3.99368 6.0449 4.00202C6.13738 4.01036 6.22572 4.04429 6.3 4.1L10.967 7.6C11.0291 7.64657 11.0795 7.70697 11.1142 7.77639C11.1489 7.84582 11.167 7.92238 11.167 8C11.167 8.07762 11.1489 8.15418 11.1142 8.22361C11.0795 8.29303 11.0291 8.35343 10.967 8.4L6.3 11.9C6.22572 11.9557 6.13738 11.9896 6.0449 11.998C5.95242 12.0063 5.85945 11.9887 5.77639 11.9472C5.69334 11.9057 5.62349 11.8419 5.57467 11.7629C5.52586 11.6839 5.5 11.5929 5.5 11.5V4.5Z',
|
|
514
|
+
fill: 'currentColor',
|
|
515
|
+
}), { size: 14, color: 'var(--dsw-alias-label-secondary)' })
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** IconChevronLeftOutlineRegular */
|
|
519
|
+
function BackIcon() {
|
|
520
|
+
return iconFrame(h('path', {
|
|
521
|
+
d: 'M10 4L6.70711 7.29289C6.31658 7.68342 6.31658 8.31658 6.70711 8.70711L10 12',
|
|
522
|
+
stroke: 'currentColor',
|
|
523
|
+
}), { size: 14, color: 'var(--dsw-alias-label-secondary)' })
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** IconEllipsisOutlineRegular */
|
|
527
|
+
function DotsIcon() {
|
|
528
|
+
return iconFrame([
|
|
529
|
+
h('path', { key: 'a', d: 'M3 9C3.55228 9 4 8.55228 4 8C4 7.44772 3.55228 7 3 7C2.44772 7 2 7.44772 2 8C2 8.55228 2.44772 9 3 9Z', fill: 'currentColor' }),
|
|
530
|
+
h('path', { key: 'b', d: 'M8 9C8.55228 9 9 8.55228 9 8C9 7.44772 8.55228 7 8 7C7.44772 7 7 7.44772 7 8C7 8.55228 7.44772 9 8 9Z', fill: 'currentColor' }),
|
|
531
|
+
h('path', { key: 'c', d: 'M13 9C13.5523 9 14 8.55228 14 8C14 7.44772 13.5523 7 13 7C12.4477 7 12 7.44772 12 8C12 8.55228 12.4477 9 13 9Z', fill: 'currentColor' }),
|
|
532
|
+
], { size: 16 })
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** IconSearchOutlineRegular */
|
|
536
|
+
function SearchIcon() {
|
|
537
|
+
return iconFrame([
|
|
538
|
+
h('path', {
|
|
539
|
+
key: 'lens',
|
|
540
|
+
d: 'M6.58727 11.8586C9.55061 11.8586 11.9529 9.45637 11.9529 6.49304C11.9529 3.5297 9.55061 1.12744 6.58727 1.12744C3.62394 1.12744 1.22168 3.5297 1.22168 6.49304C1.22168 9.45637 3.62394 11.8586 6.58727 11.8586Z',
|
|
541
|
+
stroke: 'currentColor',
|
|
542
|
+
}),
|
|
543
|
+
h('path', { key: 'handle', d: 'M10.2991 10.3933L14.7783 14.8725', stroke: 'currentColor' }),
|
|
544
|
+
], { size: 16, color: 'var(--dsw-alias-label-secondary)' })
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** IconSlidersTwoOutlineRegular — the shipped view-options glyph. */
|
|
548
|
+
function SlidersIcon() {
|
|
549
|
+
return iconFrame([
|
|
550
|
+
h('path', { key: 'l1', d: 'M2.3 5h5.85M12.05 5h1.65', stroke: 'currentColor' }),
|
|
551
|
+
h('circle', { key: 'c1', cx: 9.95, cy: 5, r: 1.45, stroke: 'currentColor' }),
|
|
552
|
+
h('path', { key: 'l2', d: 'M2.3 11h1.65M7.85 11h5.85', stroke: 'currentColor' }),
|
|
553
|
+
h('circle', { key: 'c2', cx: 5.75, cy: 11, r: 1.45, stroke: 'currentColor' }),
|
|
554
|
+
], { size: 16 })
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** IconPinFillRegular */
|
|
558
|
+
function PinIcon() {
|
|
559
|
+
return iconFrame([
|
|
560
|
+
h('path', {
|
|
561
|
+
key: 'body',
|
|
562
|
+
d: 'M9.96976 1.70572L13.1554 3.93629L10.9019 8.12317L11.5158 11.605L10.7192 12.7427L2.52767 7.00693L3.3243 5.86922L6.80612 5.25528L9.96976 1.70572Z',
|
|
563
|
+
fill: 'currentColor', stroke: 'currentColor', strokeLinejoin: 'round',
|
|
564
|
+
}),
|
|
565
|
+
h('path', {
|
|
566
|
+
key: 'needle',
|
|
567
|
+
d: 'M6.05285 9.47511C6.27284 9.16094 6.70586 9.08458 7.02003 9.30457C7.3342 9.52455 7.41055 9.95757 7.19057 10.2717L3.98587 14.4708L3.21223 13.9291L6.05285 9.47511Z',
|
|
568
|
+
fill: 'currentColor',
|
|
569
|
+
}),
|
|
570
|
+
], { size: 14 })
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** IconCloseFillRegular */
|
|
574
|
+
function CloseIcon() {
|
|
575
|
+
return iconFrame([
|
|
576
|
+
h('path', { key: 'a', d: 'M3.5 3.5L12.5 12.5', stroke: 'currentColor' }),
|
|
577
|
+
h('path', { key: 'b', d: 'M12.5 3.5L3.5 12.5', stroke: 'currentColor' }),
|
|
578
|
+
], { size: 14 })
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** IconNewChatOutlineRegular */
|
|
582
|
+
function NewChatIcon() {
|
|
583
|
+
return iconFrame([
|
|
584
|
+
h('path', {
|
|
585
|
+
key: 'bubble',
|
|
586
|
+
d: 'M2.37091 11.2501C1.58745 9.89288 1.32067 8.29835 1.61969 6.76006C1.91872 5.22177 2.76342 3.8433 3.99826 2.87846C5.2331 1.91362 6.77494 1.42737 8.33988 1.50925C9.90482 1.59113 11.3875 2.23562 12.5149 3.32406C13.6425 4.41269 14.3387 5.87206 14.4754 7.4334C14.612 8.99474 14.18 10.5529 13.2587 11.8209C12.3375 13.0888 10.9891 13.9813 9.46194 14.3337C8.18691 14.628 6.85895 14.5294 5.64989 14.0605C5.1712 13.8748 4.76962 13.4932 4.26534 13.3967C3.67413 13.2835 2.95257 13.5598 2.03794 14.3337',
|
|
587
|
+
stroke: 'currentColor',
|
|
588
|
+
}),
|
|
589
|
+
h('path', { key: 'v', d: 'M8 5V11', stroke: 'currentColor' }),
|
|
590
|
+
h('path', { key: 'h', d: 'M5 8H11', stroke: 'currentColor' }),
|
|
591
|
+
], { size: 16 })
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** IconProjectAddOutlineRegular — add-workspace. */
|
|
595
|
+
function ProjectAddIcon() {
|
|
596
|
+
return iconFrame([
|
|
597
|
+
h('path', {
|
|
598
|
+
key: 'body',
|
|
599
|
+
d: 'M5.54492 2.06738C5.91034 2.06754 6.26318 2.20149 6.53711 2.44336L7.94043 3.68164V4.7998C7.71462 4.74105 7.50367 4.63139 7.32617 4.47461L5.87598 3.19238C5.78477 3.11185 5.66658 3.06754 5.54492 3.06738H2.94922C2.67322 3.06738 2.44946 3.29145 2.44922 3.56738V12.4326C2.44927 12.7087 2.67311 12.9326 2.94922 12.9326H12.9326C13.2086 12.9325 13.4326 12.7086 13.4326 12.4326V8.53613H14.4326V12.4326C14.4326 13.2609 13.7609 13.9325 12.9326 13.9326H2.94922C2.12083 13.9326 1.44927 13.261 1.44922 12.4326V3.56738C1.44946 2.73916 2.12094 2.06738 2.94922 2.06738H5.54492Z',
|
|
600
|
+
fill: 'currentColor',
|
|
601
|
+
}),
|
|
602
|
+
h('path', { key: 'h', d: 'M9.75977 4.50208H14.5509', stroke: 'currentColor' }),
|
|
603
|
+
h('path', { key: 'v', d: 'M12.1492 6.89758L12.1492 2.10642', stroke: 'currentColor' }),
|
|
604
|
+
], { size: 16 })
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** IconCheckOutlineRegular — completed. */
|
|
608
|
+
function CheckIcon() {
|
|
609
|
+
return iconFrame(h('path', {
|
|
610
|
+
d: 'M2.25 8.5L5.49732 11.7473C5.90519 12.1552 6.57263 12.1344 6.95426 11.7018L13.75 4',
|
|
611
|
+
stroke: 'currentColor',
|
|
612
|
+
}), { size: 14 })
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** IconQuestionOutlineRegular — waiting for you. */
|
|
616
|
+
function QuestionIcon() {
|
|
617
|
+
return iconFrame([
|
|
618
|
+
h('path', {
|
|
619
|
+
key: 'circle',
|
|
620
|
+
d: 'M8 14.5C11.5899 14.5 14.5 11.5899 14.5 8C14.5 4.41015 11.5899 1.5 8 1.5C4.41015 1.5 1.5 4.41015 1.5 8C1.5 11.5899 4.41015 14.5 8 14.5Z',
|
|
621
|
+
stroke: 'currentColor',
|
|
622
|
+
}),
|
|
623
|
+
h('path', {
|
|
624
|
+
key: 'curve',
|
|
625
|
+
d: 'M5.75 6.69646C5.75 6.29865 5.88196 5.90976 6.12919 5.57899C6.37643 5.24821 6.72783 4.99041 7.13896 4.83817C7.5501 4.68593 8.0025 4.6461 8.43895 4.72371C8.87541 4.80132 9.27632 4.99289 9.59099 5.27419C9.90566 5.55549 10.12 5.91388 10.2068 6.30406C10.2936 6.69423 10.249 7.09866 10.0787 7.4662C9.90843 7.83373 9.62004 8.14787 9.25003 8.36889C9.19476 8.4019 9.13803 8.43262 9.08004 8.46099C8.52566 8.73217 8 9.20817 8 9.82532',
|
|
626
|
+
stroke: 'currentColor',
|
|
627
|
+
}),
|
|
628
|
+
h('path', { key: 'dot', d: 'M8 10.7416V11.7416', stroke: 'currentColor' }),
|
|
629
|
+
], { size: 14 })
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** IconWarningOutlineRegular — failure or interruption. */
|
|
633
|
+
function WarningIcon() {
|
|
634
|
+
return iconFrame([
|
|
635
|
+
h('path', {
|
|
636
|
+
key: 'circle',
|
|
637
|
+
d: 'M8 14.5C11.5899 14.5 14.5 11.5899 14.5 8C14.5 4.41015 11.5899 1.5 8 1.5C4.41015 1.5 1.5 4.41015 1.5 8C1.5 11.5899 4.41015 14.5 8 14.5Z',
|
|
638
|
+
stroke: 'currentColor',
|
|
639
|
+
}),
|
|
640
|
+
h('path', { key: 'bar', d: 'M8 4.29199V9.79199', stroke: 'currentColor' }),
|
|
641
|
+
h('path', { key: 'dot', d: 'M8 10.708V11.708', stroke: 'currentColor' }),
|
|
642
|
+
], { size: 14 })
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* One title line: the outer span clips, the inner span scrolls on hover.
|
|
647
|
+
* The marquee only starts for text that does not fit, so short names stay
|
|
648
|
+
* still and keep their ellipsis when narrowed.
|
|
649
|
+
*/
|
|
650
|
+
function Title(props) {
|
|
651
|
+
const className = props.className ?? 'dfs-title'
|
|
652
|
+
return h('span', { className, title: props.title ?? props.text },
|
|
653
|
+
h('span', { className: 'dfs-titleText' }, props.text))
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
class RenderBoundary extends React.Component {
|
|
657
|
+
constructor(props) {
|
|
658
|
+
super(props)
|
|
659
|
+
this.state = { message: null }
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
static getDerivedStateFromError(error) {
|
|
663
|
+
return { message: error instanceof Error ? error.message : String(error) }
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
componentDidCatch(error) {
|
|
667
|
+
console.error('[folder-sidebar] render failed', error)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
render() {
|
|
671
|
+
if (this.state.message !== null) {
|
|
672
|
+
return h('div', {
|
|
673
|
+
className: 'dfs-empty',
|
|
674
|
+
style: { color: 'var(--dsw-alias-state-error-primary)' },
|
|
675
|
+
}, 'folder-sidebar: ', this.state.message)
|
|
676
|
+
}
|
|
677
|
+
return this.props.children
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
return {
|
|
685
|
+
inject: ['slots', 'locale', 'uiWorkspace', 'workspaces', 'sessions'],
|
|
686
|
+
apply(ctx) {
|
|
687
|
+
ctx.effect(() => ctx.locale.register(NS, DICTS))
|
|
688
|
+
const t = ctx.locale.bind(NS)
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* The leading status cell: a fixed 16px slot on every Session row so
|
|
692
|
+
* titles stay aligned whether or not the row has a state. Completed,
|
|
693
|
+
* waiting and failure reuse the shipped glyphs; running is the spinner.
|
|
694
|
+
*/
|
|
695
|
+
function statusIcon(kind) {
|
|
696
|
+
if (kind === 'completed') return CheckIcon
|
|
697
|
+
if (kind === 'waiting') return QuestionIcon
|
|
698
|
+
if (kind === 'error') return WarningIcon
|
|
699
|
+
return undefined
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function Indicator({ kind, title }) {
|
|
703
|
+
const Glyph = statusIcon(kind)
|
|
704
|
+
if (Glyph !== undefined) {
|
|
705
|
+
return h('span', {
|
|
706
|
+
className: 'dfs-status dfs-glyph',
|
|
707
|
+
style: { color: GLYPH_COLOR[kind] },
|
|
708
|
+
title,
|
|
709
|
+
'aria-label': title,
|
|
710
|
+
role: 'img',
|
|
711
|
+
}, h(Glyph))
|
|
712
|
+
}
|
|
713
|
+
if (kind === undefined) {
|
|
714
|
+
return h('span', { className: 'dfs-status', title, 'aria-label': title, role: 'img' },
|
|
715
|
+
h('span', { className: 'dfs-statusDot is-none' }))
|
|
716
|
+
}
|
|
717
|
+
if (kind === 'running') {
|
|
718
|
+
return h('span', { className: 'dfs-status', title, 'aria-label': title, role: 'img' },
|
|
719
|
+
h('span', { className: 'dfs-spinner' }))
|
|
720
|
+
}
|
|
721
|
+
return h('span', { className: 'dfs-status', title, 'aria-label': title, role: 'img' },
|
|
722
|
+
h('span', { className: `dfs-statusDot is-${kind}` }))
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** A Workspace row's single status cell: the folder icon itself. */
|
|
726
|
+
function FolderGlyph({ kind, label }) {
|
|
727
|
+
const color = kind === 'error' ? 'var(--dsw-alias-state-error-primary)'
|
|
728
|
+
: kind === 'waiting' ? 'var(--dsw-alias-state-warn-primary)'
|
|
729
|
+
: kind === 'completed' ? 'var(--dsw-alias-state-success-primary)'
|
|
730
|
+
: kind === 'stopped' ? 'var(--dsw-alias-state-idle-primary)'
|
|
731
|
+
: 'var(--dsw-alias-label-secondary)'
|
|
732
|
+
const Glyph = statusIcon(kind)
|
|
733
|
+
if (Glyph !== undefined) {
|
|
734
|
+
return h('span', {
|
|
735
|
+
className: 'dfs-status dfs-statusFolder dfs-glyph',
|
|
736
|
+
style: { color: GLYPH_COLOR[kind] },
|
|
737
|
+
title: label,
|
|
738
|
+
'aria-label': label,
|
|
739
|
+
role: 'img',
|
|
740
|
+
}, h(Glyph))
|
|
741
|
+
}
|
|
742
|
+
if (kind === 'running') {
|
|
743
|
+
return h('span', {
|
|
744
|
+
className: 'dfs-status dfs-statusFolder',
|
|
745
|
+
title: label,
|
|
746
|
+
'aria-label': label,
|
|
747
|
+
role: 'img',
|
|
748
|
+
}, h('span', { className: 'dfs-spinner' }))
|
|
749
|
+
}
|
|
750
|
+
return h('span', {
|
|
751
|
+
className: 'dfs-status dfs-statusFolder',
|
|
752
|
+
title: label,
|
|
753
|
+
'aria-label': label,
|
|
754
|
+
role: 'img',
|
|
755
|
+
}, h('span', { className: 'dfs-folderGlyph' }, h(FolderIcon, { color })))
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function FolderSidebar(props) {
|
|
759
|
+
const wide = props.wide !== false
|
|
760
|
+
const expandSidebar = props.expandSidebar
|
|
761
|
+
const useWorkspaces = props.useWorkspaces
|
|
762
|
+
const useSessions = props.useSessions
|
|
763
|
+
const useSessionStatus = props.useSessionStatus
|
|
764
|
+
|
|
765
|
+
const [openId, setOpenId] = React.useState(null)
|
|
766
|
+
const [query, setQuery] = React.useState('')
|
|
767
|
+
const [menuFor, setMenuFor] = React.useState(null)
|
|
768
|
+
const [rename, setRename] = React.useState(null)
|
|
769
|
+
const [confirm, setConfirm] = React.useState(null)
|
|
770
|
+
const [archivedFilter, setArchivedFilter] = React.useState(() => readArchivedFilter())
|
|
771
|
+
const [notice, setNotice] = React.useState(null)
|
|
772
|
+
const [contentHits, setContentHits] = React.useState([])
|
|
773
|
+
const [searching, setSearching] = React.useState(false)
|
|
774
|
+
const [hoverFor, setHoverFor] = React.useState(null)
|
|
775
|
+
const [searchOpen, setSearchOpen] = React.useState(false)
|
|
776
|
+
const searchInput = React.useRef(null)
|
|
777
|
+
const hoverTimer = React.useRef(undefined)
|
|
778
|
+
const [sortState, setSortState] = React.useState(() => readSortState())
|
|
779
|
+
const folderSort = sortState.folders
|
|
780
|
+
const sessionSort = sortState.sessions
|
|
781
|
+
|
|
782
|
+
const workspaceItems = typeof useWorkspaces === 'function' ? useWorkspaces((state) => state.items) : undefined
|
|
783
|
+
const archivedIds = typeof useWorkspaces === 'function' ? useWorkspaces((state) => state.archivedSessionIds) : undefined
|
|
784
|
+
const pinnedIds = typeof useWorkspaces === 'function' ? useWorkspaces((state) => state.pinnedSessionIds) : undefined
|
|
785
|
+
const list = typeof useSessions === 'function' ? useSessions((state) => state) : undefined
|
|
786
|
+
const projections = typeof useSessions === 'function' ? useSessions((state) => state.projectionsBySession) : undefined
|
|
787
|
+
const statusMap = typeof useSessionStatus === 'function' ? useSessionStatus((state) => state) : undefined
|
|
788
|
+
|
|
789
|
+
const workspaces = Array.isArray(workspaceItems) ? workspaceItems : []
|
|
790
|
+
const archived = React.useMemo(() => new Set(archivedIds ?? []), [archivedIds])
|
|
791
|
+
const pinned = React.useMemo(() => new Set(pinnedIds ?? []), [pinnedIds])
|
|
792
|
+
|
|
793
|
+
const statusOf = React.useCallback((sessionId) => {
|
|
794
|
+
if (statusMap === undefined || statusMap === null) return undefined
|
|
795
|
+
return typeof statusMap.get === 'function' ? statusMap.get(sessionId) : undefined
|
|
796
|
+
}, [statusMap])
|
|
797
|
+
|
|
798
|
+
const projectionOf = React.useCallback((sessionId) => {
|
|
799
|
+
if (projections === undefined || projections === null) return undefined
|
|
800
|
+
const cell = projections[sessionId]
|
|
801
|
+
const values = cell === undefined || cell === null ? undefined : cell.values
|
|
802
|
+
return values === undefined || values === null ? undefined : values[TURN_KEY]
|
|
803
|
+
}, [projections])
|
|
804
|
+
|
|
805
|
+
const currentId = React.useMemo(() => {
|
|
806
|
+
const byId = list === undefined || list === null ? undefined : list.byId
|
|
807
|
+
if (byId === undefined || byId === null) return undefined
|
|
808
|
+
for (const id of Object.keys(byId)) {
|
|
809
|
+
const summary = byId[id]
|
|
810
|
+
if (summary !== null && typeof summary === 'object' && (summary.retainedBy?.mainView ?? 0) > 0) return id
|
|
811
|
+
}
|
|
812
|
+
return undefined
|
|
813
|
+
}, [list])
|
|
814
|
+
|
|
815
|
+
const currentWorkspaceId = React.useMemo(() => {
|
|
816
|
+
if (currentId === undefined) return undefined
|
|
817
|
+
for (const workspace of workspaces) {
|
|
818
|
+
if (Array.isArray(workspace.sessionIds) && workspace.sessionIds.includes(currentId)) return workspace.workspaceId
|
|
819
|
+
}
|
|
820
|
+
return undefined
|
|
821
|
+
}, [workspaces, currentId])
|
|
822
|
+
|
|
823
|
+
React.useEffect(() => {
|
|
824
|
+
if (currentWorkspaceId === undefined) return
|
|
825
|
+
setOpenId((previous) => (previous === null ? currentWorkspaceId : previous))
|
|
826
|
+
}, [currentWorkspaceId])
|
|
827
|
+
|
|
828
|
+
// Close transient UI when the region loses the selection it belongs to.
|
|
829
|
+
React.useEffect(() => {
|
|
830
|
+
if (menuFor === null) return undefined
|
|
831
|
+
const onDown = (event) => {
|
|
832
|
+
const target = event.target
|
|
833
|
+
if (target === null || typeof target.closest !== 'function') { setMenuFor(null); return }
|
|
834
|
+
// A press inside the panel must reach the item; a press on the
|
|
835
|
+
// trigger is the trigger's own toggle. Anything else dismisses.
|
|
836
|
+
const insideMenu = target.closest('[data-dfs-menu]') !== null
|
|
837
|
+
const onTrigger = target.closest('[data-dfs-menu-trigger]') !== null
|
|
838
|
+
if (!insideMenu && !onTrigger) setMenuFor(null)
|
|
839
|
+
}
|
|
840
|
+
const onKey = (event) => { if (event.key === 'Escape') setMenuFor(null) }
|
|
841
|
+
const onScroll = () => setMenuFor(null)
|
|
842
|
+
document.addEventListener('pointerdown', onDown)
|
|
843
|
+
document.addEventListener('keydown', onKey)
|
|
844
|
+
document.addEventListener('scroll', onScroll, true)
|
|
845
|
+
return () => {
|
|
846
|
+
document.removeEventListener('pointerdown', onDown)
|
|
847
|
+
document.removeEventListener('keydown', onKey)
|
|
848
|
+
document.removeEventListener('scroll', onScroll, true)
|
|
849
|
+
}
|
|
850
|
+
}, [menuFor])
|
|
851
|
+
|
|
852
|
+
/** One transient bar; an optional action keeps "撤销" reachable. */
|
|
853
|
+
const notify = (message, action) => setNotice({ message, action: action ?? null })
|
|
854
|
+
|
|
855
|
+
React.useEffect(() => {
|
|
856
|
+
if (notice === null) return undefined
|
|
857
|
+
const timer = setTimeout(() => setNotice(null), notice.action === null ? 2200 : 6000)
|
|
858
|
+
return () => clearTimeout(timer)
|
|
859
|
+
}, [notice])
|
|
860
|
+
|
|
861
|
+
// Host content search, debounced; titles stay local and instant.
|
|
862
|
+
React.useEffect(() => {
|
|
863
|
+
const text = query.trim()
|
|
864
|
+
if (text === '' || typeof ctx.sessions?.search !== 'function') {
|
|
865
|
+
setContentHits([])
|
|
866
|
+
setSearching(false)
|
|
867
|
+
return undefined
|
|
868
|
+
}
|
|
869
|
+
const controller = new AbortController()
|
|
870
|
+
setSearching(true)
|
|
871
|
+
const timer = setTimeout(() => {
|
|
872
|
+
Promise.resolve(ctx.sessions.search(text, controller.signal))
|
|
873
|
+
.then((result) => {
|
|
874
|
+
if (controller.signal.aborted) return
|
|
875
|
+
const items = result?.ok === true && Array.isArray(result.value?.items) ? result.value.items : []
|
|
876
|
+
setContentHits(items)
|
|
877
|
+
setSearching(false)
|
|
878
|
+
})
|
|
879
|
+
.catch(() => { if (!controller.signal.aborted) { setContentHits([]); setSearching(false) } })
|
|
880
|
+
}, 250)
|
|
881
|
+
return () => {
|
|
882
|
+
clearTimeout(timer)
|
|
883
|
+
controller.abort()
|
|
884
|
+
setSearching(false)
|
|
885
|
+
}
|
|
886
|
+
}, [query])
|
|
887
|
+
|
|
888
|
+
/** Sessions that belong to no Workspace (shipped "Ungrouped" bucket). */
|
|
889
|
+
const ungroupedWorkspace = React.useMemo(() => {
|
|
890
|
+
const byId = list === undefined || list === null ? undefined : list.byId
|
|
891
|
+
const ids = list === undefined || list === null || !Array.isArray(list.ids) ? [] : list.ids
|
|
892
|
+
const accounted = new Set()
|
|
893
|
+
for (const workspace of workspaces) {
|
|
894
|
+
for (const id of Array.isArray(workspace.sessionIds) ? workspace.sessionIds : []) accounted.add(id)
|
|
895
|
+
}
|
|
896
|
+
const members = []
|
|
897
|
+
for (const id of ids) {
|
|
898
|
+
const summary = byId === undefined || byId === null ? undefined : byId[id]
|
|
899
|
+
if (summary === null || typeof summary !== 'object') continue
|
|
900
|
+
if (accounted.has(id)) continue
|
|
901
|
+
if (summary.origin === 'subagent') continue
|
|
902
|
+
members.push(summary)
|
|
903
|
+
}
|
|
904
|
+
return { workspaceId: UNGROUPED, path: '', title: t('group.ungrouped'), sessionIds: members.map((summary) => summary.id) }
|
|
905
|
+
}, [list, workspaces, t])
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Rows per Workspace, derived once per render instead of once per
|
|
909
|
+
* caller: the list, its status cell and the hover card all read it.
|
|
910
|
+
*/
|
|
911
|
+
const rowsIndex = React.useMemo(() => {
|
|
912
|
+
const byId = list === undefined || list === null ? undefined : list.byId
|
|
913
|
+
const collect = (ids) => {
|
|
914
|
+
const rows = []
|
|
915
|
+
for (const id of Array.isArray(ids) ? ids : []) {
|
|
916
|
+
const summary = byId === undefined || byId === null ? undefined : byId[id]
|
|
917
|
+
if (summary === null || typeof summary !== 'object') continue
|
|
918
|
+
if (summary.origin === 'subagent') continue
|
|
919
|
+
const isArchived = archived.has(id)
|
|
920
|
+
if (archivedFilter === 'hide' && isArchived) continue
|
|
921
|
+
if (archivedFilter === 'only' && !isArchived) continue
|
|
922
|
+
if (summary.blank === true && id !== currentId) continue
|
|
923
|
+
rows.push(summary)
|
|
924
|
+
}
|
|
925
|
+
rows.sort((a, b) => {
|
|
926
|
+
const pinDelta = (pinned.has(b.id) ? 1 : 0) - (pinned.has(a.id) ? 1 : 0)
|
|
927
|
+
if (pinDelta !== 0) return pinDelta
|
|
928
|
+
return (b.updatedAt ?? 0) - (a.updatedAt ?? 0)
|
|
929
|
+
})
|
|
930
|
+
return rows
|
|
931
|
+
}
|
|
932
|
+
const index = new Map()
|
|
933
|
+
for (const workspace of workspaces) index.set(workspace.workspaceId, collect(workspace.sessionIds))
|
|
934
|
+
index.set(UNGROUPED, collect(ungroupedWorkspace.sessionIds))
|
|
935
|
+
return index
|
|
936
|
+
}, [list, archived, pinned, currentId, archivedFilter, workspaces, ungroupedWorkspace])
|
|
937
|
+
|
|
938
|
+
const rowsFor = (workspace) => rowsIndex.get(workspace.workspaceId) ?? []
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
const workspaceOf = React.useCallback((sessionId) => {
|
|
942
|
+
for (const workspace of workspaces) {
|
|
943
|
+
if (Array.isArray(workspace.sessionIds) && workspace.sessionIds.includes(sessionId)) return workspace
|
|
944
|
+
}
|
|
945
|
+
return undefined
|
|
946
|
+
}, [workspaces])
|
|
947
|
+
|
|
948
|
+
const titleOf = React.useCallback((summary) => {
|
|
949
|
+
return typeof summary.displayTitle === 'string' && summary.displayTitle !== ''
|
|
950
|
+
? summary.displayTitle
|
|
951
|
+
: t('session.untitled')
|
|
952
|
+
}, [t])
|
|
953
|
+
|
|
954
|
+
const displayNameOf = (workspace) => (typeof workspace.title === 'string' && workspace.title !== ''
|
|
955
|
+
? workspace.title
|
|
956
|
+
: baseName(workspace.path))
|
|
957
|
+
|
|
958
|
+
/** Newest Session activity in a Workspace, used by the "recent" sort. */
|
|
959
|
+
const lastActivityOf = (workspace) => {
|
|
960
|
+
const byId = list === undefined || list === null ? undefined : list.byId
|
|
961
|
+
let latest = 0
|
|
962
|
+
for (const id of Array.isArray(workspace.sessionIds) ? workspace.sessionIds : []) {
|
|
963
|
+
const summary = byId === undefined || byId === null ? undefined : byId[id]
|
|
964
|
+
const time = summary === undefined || summary === null ? undefined : summary.updatedAt
|
|
965
|
+
if (typeof time === 'number' && time > latest) latest = time
|
|
966
|
+
}
|
|
967
|
+
return latest
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const orderedWorkspaces = React.useMemo(
|
|
971
|
+
() => sortWorkspaces(workspaces, folderSort, { nameOf: displayNameOf, activityOf: lastActivityOf }),
|
|
972
|
+
[workspaces, folderSort, list],
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
/** Pinned rows stay first in every mode; direction applies inside groups. */
|
|
976
|
+
/** Session ordering for the active level (pure comparator lives up top). */
|
|
977
|
+
const orderSessions = (rows) => sortSessions(rows, sessionSort, {
|
|
978
|
+
titleOf,
|
|
979
|
+
isPinned: (row) => pinned.has(row.id),
|
|
980
|
+
updatedAtOf: (row) => row.updatedAt,
|
|
981
|
+
})
|
|
982
|
+
|
|
983
|
+
/** The level in view owns the sort: folders outside, Sessions inside. */
|
|
984
|
+
const activeLevel = openId === null ? 'folders' : 'sessions'
|
|
985
|
+
const activeSort = sortState[activeLevel]
|
|
986
|
+
|
|
987
|
+
// Folder drag-and-drop: only meaningful when the Host order is the
|
|
988
|
+
// rendered order (manual + ascending) and the flat list is visible.
|
|
989
|
+
const [dragId, setDragId] = React.useState(null)
|
|
990
|
+
const [dropEdge, setDropEdge] = React.useState(null)
|
|
991
|
+
const canReorder = folderSort.mode === 'manual' && folderSort.dir === 'asc'
|
|
992
|
+
&& openId === null && query.trim() === ''
|
|
993
|
+
const clearDrag = () => {
|
|
994
|
+
setDragId(null)
|
|
995
|
+
setDropEdge(null)
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
React.useEffect(() => {
|
|
999
|
+
if (!canReorder) clearDrag()
|
|
1000
|
+
}, [canReorder])
|
|
1001
|
+
|
|
1002
|
+
/** Land one Workspace before a Host-order anchor (`undefined` appends). */
|
|
1003
|
+
const dropOn = (targetId, edge) => {
|
|
1004
|
+
if (dragId === null || dragId === targetId) { clearDrag(); return }
|
|
1005
|
+
const ids = workspaces.map((workspace) => workspace.workspaceId)
|
|
1006
|
+
const targetIndex = ids.indexOf(targetId)
|
|
1007
|
+
if (targetIndex < 0) { clearDrag(); return }
|
|
1008
|
+
const beforeId = edge === 'before' ? ids[targetIndex] : ids[targetIndex + 1]
|
|
1009
|
+
// Dropping onto the neighbour it already precedes is a no-op.
|
|
1010
|
+
if (beforeId !== dragId) moveWorkspace(dragId, beforeId)
|
|
1011
|
+
clearDrag()
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const commitSort = (patch) => {
|
|
1015
|
+
const next = {
|
|
1016
|
+
...sortState,
|
|
1017
|
+
[activeLevel]: { ...sortState[activeLevel], ...patch },
|
|
1018
|
+
}
|
|
1019
|
+
setSortState(next)
|
|
1020
|
+
writeSortState(next)
|
|
1021
|
+
setMenuFor(null)
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const chooseMode = (mode) => commitSort({ mode, dir: SORT_NATURAL_DIR[mode] })
|
|
1025
|
+
|
|
1026
|
+
const modeItems = () => SORT_MODES.map((mode) => ({
|
|
1027
|
+
key: `mode:${mode}`,
|
|
1028
|
+
label: t(`sort.${mode}`),
|
|
1029
|
+
check: mode === activeSort.mode,
|
|
1030
|
+
onSelect: () => chooseMode(mode),
|
|
1031
|
+
}))
|
|
1032
|
+
|
|
1033
|
+
const dirItems = () => SORT_DIRS.map((dir) => ({
|
|
1034
|
+
key: `dir:${dir}`,
|
|
1035
|
+
label: `${t(`sort.${dir}`)} ${dir === 'desc' ? '↓' : '↑'}`,
|
|
1036
|
+
check: dir === activeSort.dir,
|
|
1037
|
+
onSelect: () => commitSort({ dir }),
|
|
1038
|
+
}))
|
|
1039
|
+
|
|
1040
|
+
const folderKindOf = React.useCallback((workspace) => {
|
|
1041
|
+
const candidates = []
|
|
1042
|
+
for (const summary of rowsFor(workspace)) {
|
|
1043
|
+
const kind = indicatorOf(statusOf(summary.id), projectionOf(summary.id))
|
|
1044
|
+
if (kind === undefined) continue
|
|
1045
|
+
const projection = projectionOf(summary.id)
|
|
1046
|
+
const since = projection !== undefined && projection !== null
|
|
1047
|
+
&& typeof projection.since === 'number' && projection.since > 0
|
|
1048
|
+
? projection.since
|
|
1049
|
+
: (summary.updatedAt ?? 0)
|
|
1050
|
+
candidates.push({ kind, since })
|
|
1051
|
+
}
|
|
1052
|
+
return pickFolderKind(candidates)
|
|
1053
|
+
}, [rowsFor, statusOf, projectionOf])
|
|
1054
|
+
|
|
1055
|
+
const labels = {}
|
|
1056
|
+
for (const kind of INDICATOR_PRIORITY) labels[kind] = t(INDICATOR_LABEL_KEY[kind])
|
|
1057
|
+
|
|
1058
|
+
/** The shipped browser names the exact interaction; mirror that. */
|
|
1059
|
+
const waitingLabel = (status) => {
|
|
1060
|
+
const kind = status !== undefined && status !== null && status.pendingInteraction !== undefined && status.pendingInteraction !== null
|
|
1061
|
+
? status.pendingInteraction.kind
|
|
1062
|
+
: undefined
|
|
1063
|
+
if (kind === 'approval') return t('status.waitingApproval')
|
|
1064
|
+
if (kind === 'plan-review') return t('status.waitingPlanReview')
|
|
1065
|
+
if (kind === 'question') return t('status.waitingAnswer')
|
|
1066
|
+
return t('status.waiting')
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/** Every status one Session carries, for the hover card. */
|
|
1070
|
+
const statusRowsOf = (status, projection, updatedAt) => {
|
|
1071
|
+
const rows = []
|
|
1072
|
+
if (status !== undefined && status !== null
|
|
1073
|
+
&& status.pendingInteraction !== undefined && status.pendingInteraction !== null) {
|
|
1074
|
+
rows.push({ key: 'waiting', kind: 'waiting', label: waitingLabel(status) })
|
|
1075
|
+
}
|
|
1076
|
+
if (status !== undefined && status !== null && status.running === true) {
|
|
1077
|
+
rows.push({ key: 'running', kind: 'running', label: labels.running })
|
|
1078
|
+
}
|
|
1079
|
+
const state = projection === undefined || projection === null ? undefined : projection.state
|
|
1080
|
+
if (state === 'failed' || state === 'interrupted') rows.push({ key: 'error', kind: 'error', label: labels.error })
|
|
1081
|
+
if (status !== undefined && status !== null && status.completionUnread === true) {
|
|
1082
|
+
rows.push({ key: 'completed', kind: 'completed', label: labels.completed })
|
|
1083
|
+
}
|
|
1084
|
+
if (state === 'stopped') rows.push({ key: 'stopped', kind: 'stopped', label: labels.stopped })
|
|
1085
|
+
if (rows.length === 0) rows.push({ key: 'idle', kind: undefined, label: t('status.idle') })
|
|
1086
|
+
rows.push({ key: 'time', kind: undefined, label: relativeTime(updatedAt) })
|
|
1087
|
+
return rows
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/** Which states a Workspace contains and how many Sessions carry each. */
|
|
1091
|
+
const folderStatusRows = (workspace) => {
|
|
1092
|
+
const counts = new Map()
|
|
1093
|
+
for (const summary of rowsFor(workspace)) {
|
|
1094
|
+
const kind = indicatorOf(statusOf(summary.id), projectionOf(summary.id))
|
|
1095
|
+
if (kind === undefined) continue
|
|
1096
|
+
counts.set(kind, (counts.get(kind) ?? 0) + 1)
|
|
1097
|
+
}
|
|
1098
|
+
const rows = []
|
|
1099
|
+
for (const kind of INDICATOR_PRIORITY) {
|
|
1100
|
+
const count = counts.get(kind)
|
|
1101
|
+
if (count === undefined) continue
|
|
1102
|
+
rows.push({ key: kind, kind, label: `${labels[kind]} ×${count}` })
|
|
1103
|
+
}
|
|
1104
|
+
if (rows.length === 0) rows.push({ key: 'idle', kind: undefined, label: t('status.idle') })
|
|
1105
|
+
return rows
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const cancelHover = () => {
|
|
1109
|
+
if (hoverTimer.current !== undefined) {
|
|
1110
|
+
clearTimeout(hoverTimer.current)
|
|
1111
|
+
hoverTimer.current = undefined
|
|
1112
|
+
}
|
|
1113
|
+
setHoverFor(null)
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const scheduleHover = (key, rows, event) => {
|
|
1117
|
+
if (menuFor !== null || dragId !== null) return
|
|
1118
|
+
if (hoverTimer.current !== undefined) clearTimeout(hoverTimer.current)
|
|
1119
|
+
const rect = event.currentTarget.getBoundingClientRect()
|
|
1120
|
+
hoverTimer.current = setTimeout(() => {
|
|
1121
|
+
hoverTimer.current = undefined
|
|
1122
|
+
const width = 200
|
|
1123
|
+
const height = rows.length * 20 + 16
|
|
1124
|
+
const viewportW = typeof window === 'undefined' ? 1280 : window.innerWidth
|
|
1125
|
+
const viewportH = typeof window === 'undefined' ? 800 : window.innerHeight
|
|
1126
|
+
setHoverFor({
|
|
1127
|
+
key,
|
|
1128
|
+
rows,
|
|
1129
|
+
left: Math.max(8, Math.min(rect.left + 8, viewportW - width - 8)),
|
|
1130
|
+
top: Math.max(8, Math.min(rect.bottom + 6, viewportH - height - 8)),
|
|
1131
|
+
})
|
|
1132
|
+
}, 350)
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
React.useEffect(() => () => {
|
|
1136
|
+
if (hoverTimer.current !== undefined) clearTimeout(hoverTimer.current)
|
|
1137
|
+
}, [])
|
|
1138
|
+
|
|
1139
|
+
/** Reveal a clipped title by scrolling it while the row is hovered. */
|
|
1140
|
+
const startMarquee = (event) => {
|
|
1141
|
+
const row = event.currentTarget
|
|
1142
|
+
if (row === null || typeof row.querySelector !== 'function') return
|
|
1143
|
+
const text = row.querySelector('.dfs-titleText')
|
|
1144
|
+
const box = text === null ? null : text.parentElement
|
|
1145
|
+
if (text === null || box === null) return
|
|
1146
|
+
const shift = marqueeShiftOf(text.scrollWidth, box.clientWidth)
|
|
1147
|
+
if (shift === 0) return
|
|
1148
|
+
text.style.setProperty('--dfs-marquee-shift', `-${shift}px`)
|
|
1149
|
+
text.style.setProperty('--dfs-marquee-duration', `${marqueeDurationOf(shift)}ms`)
|
|
1150
|
+
box.classList.add('is-marquee')
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const stopMarquee = (event) => {
|
|
1154
|
+
const row = event.currentTarget
|
|
1155
|
+
if (row === null || typeof row.querySelector !== 'function') return
|
|
1156
|
+
const text = row.querySelector('.dfs-titleText')
|
|
1157
|
+
const box = text === null ? null : text.parentElement
|
|
1158
|
+
if (text === null || box === null) return
|
|
1159
|
+
box.classList.remove('is-marquee')
|
|
1160
|
+
text.style.removeProperty('--dfs-marquee-shift')
|
|
1161
|
+
text.style.removeProperty('--dfs-marquee-duration')
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
React.useEffect(() => {
|
|
1166
|
+
if (hoverFor === null) return undefined
|
|
1167
|
+
const onScroll = () => setHoverFor(null)
|
|
1168
|
+
document.addEventListener('scroll', onScroll, true)
|
|
1169
|
+
return () => document.removeEventListener('scroll', onScroll, true)
|
|
1170
|
+
}, [hoverFor])
|
|
1171
|
+
|
|
1172
|
+
const openSession = (sessionId) => {
|
|
1173
|
+
setMenuFor(null)
|
|
1174
|
+
setQuery('')
|
|
1175
|
+
setSearchOpen(false)
|
|
1176
|
+
if (typeof ctx.uiWorkspace?.openSession === 'function') ctx.uiWorkspace.openSession(sessionId)
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const startSession = (workspaceId) => {
|
|
1180
|
+
setMenuFor(null)
|
|
1181
|
+
if (typeof ctx.uiWorkspace?.startSession === 'function') ctx.uiWorkspace.startSession(workspaceId)
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const addWorkspace = () => {
|
|
1185
|
+
const pickDirectory = ctx.uiWorkspace?.pickDirectory
|
|
1186
|
+
if (typeof pickDirectory !== 'function') return
|
|
1187
|
+
Promise.resolve(pickDirectory.call(ctx.uiWorkspace))
|
|
1188
|
+
.then((path) => {
|
|
1189
|
+
if (typeof path !== 'string' || path === '') return undefined
|
|
1190
|
+
return ctx.workspaces?.create?.({ path })
|
|
1191
|
+
})
|
|
1192
|
+
.catch((error) => reportFailure(error, 'notice.addWorkspaceFailed'))
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/** Every failed action is both logged and shown; silence hides bugs. */
|
|
1196
|
+
const reportFailure = (error, messageKey) => {
|
|
1197
|
+
console.warn('[folder-sidebar] action failed', error)
|
|
1198
|
+
notify(t(messageKey))
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const run = (promise, messageKey) => {
|
|
1202
|
+
Promise.resolve(promise).catch((error) => reportFailure(error, messageKey))
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
const togglePin = (sessionId) => {
|
|
1206
|
+
setMenuFor(null)
|
|
1207
|
+
const call = pinned.has(sessionId) ? ctx.uiWorkspace?.unpinSession : ctx.uiWorkspace?.pinSession
|
|
1208
|
+
if (typeof call !== 'function') return
|
|
1209
|
+
run(call.call(ctx.uiWorkspace, sessionId), 'notice.pinFailed')
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
const archiveNow = (sessionId, stopActivity) => {
|
|
1213
|
+
setConfirm(null)
|
|
1214
|
+
setMenuFor(null)
|
|
1215
|
+
const archive = ctx.uiWorkspace?.archiveSession
|
|
1216
|
+
if (typeof archive !== 'function') return
|
|
1217
|
+
Promise.resolve(archive.call(ctx.uiWorkspace, sessionId, stopActivity === true ? { stopActivity: true } : undefined))
|
|
1218
|
+
.then(() => notify(t('notice.archived'), { label: t('action.undo'), onSelect: () => unarchive(sessionId) }))
|
|
1219
|
+
.catch((error) => {
|
|
1220
|
+
const code = error !== null && typeof error === 'object' && typeof error.code === 'string' ? error.code : ''
|
|
1221
|
+
// The Host refuses a session with running work unless told to stop it.
|
|
1222
|
+
if (code.includes('session-active') || String(error?.message ?? '').includes('session-active')) setConfirm({ kind: 'archive', sessionId })
|
|
1223
|
+
else reportFailure(error, 'notice.archiveFailed')
|
|
1224
|
+
})
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
const fork = (sessionId) => {
|
|
1228
|
+
setMenuFor(null)
|
|
1229
|
+
const call = ctx.uiWorkspace?.forkSession
|
|
1230
|
+
if (typeof call === 'function') run(call.call(ctx.uiWorkspace, sessionId), 'notice.forkFailed')
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const copyId = (sessionId) => {
|
|
1234
|
+
setMenuFor(null)
|
|
1235
|
+
const clipboard = typeof navigator === 'undefined' ? undefined : navigator.clipboard
|
|
1236
|
+
if (clipboard === undefined || typeof clipboard.writeText !== 'function') return
|
|
1237
|
+
Promise.resolve(clipboard.writeText(sessionId))
|
|
1238
|
+
.then(() => notify(t('copied')))
|
|
1239
|
+
.catch(() => {})
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const startRenameSession = (summary) => {
|
|
1243
|
+
setMenuFor(null)
|
|
1244
|
+
setRename({ kind: 'session', id: summary.id, value: titleOf(summary) })
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const startRenameWorkspace = (workspace) => {
|
|
1248
|
+
setMenuFor(null)
|
|
1249
|
+
setRename({ kind: 'workspace', id: workspace.workspaceId, value: workspace.title || baseName(workspace.path) })
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const commitRename = () => {
|
|
1253
|
+
const draft = rename
|
|
1254
|
+
if (draft === null) return
|
|
1255
|
+
setRename(null)
|
|
1256
|
+
const value = typeof draft.value === 'string' ? draft.value.trim() : ''
|
|
1257
|
+
if (value === '') return
|
|
1258
|
+
if (draft.kind === 'workspace') {
|
|
1259
|
+
run(ctx.workspaces?.rename?.(draft.id, value), 'notice.renameFailed')
|
|
1260
|
+
return
|
|
1261
|
+
}
|
|
1262
|
+
const sessions = ctx.sessions
|
|
1263
|
+
if (sessions === undefined || typeof sessions.using !== 'function') return
|
|
1264
|
+
Promise.resolve(sessions.using(draft.id, { source: 'workspaceOperation' }, (reference) => reference.binding.session.rename(value)))
|
|
1265
|
+
.then((result) => {
|
|
1266
|
+
if (result !== null && typeof result === 'object' && result.ok === false) throw new Error(result.error?.message ?? 'rename failed')
|
|
1267
|
+
})
|
|
1268
|
+
.catch((error) => reportFailure(error, 'notice.renameFailed'))
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const deleteWorkspace = (workspaceId) => {
|
|
1272
|
+
setConfirm(null)
|
|
1273
|
+
setMenuFor(null)
|
|
1274
|
+
const remove = ctx.workspaces?.delete
|
|
1275
|
+
if (typeof remove !== 'function') return
|
|
1276
|
+
Promise.resolve(remove.call(ctx.workspaces, workspaceId))
|
|
1277
|
+
.then(() => setOpenId(null))
|
|
1278
|
+
.catch((error) => reportFailure(error, 'notice.deleteFailed'))
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/** Move one Workspace before another; `undefined` appends. */
|
|
1282
|
+
const moveWorkspace = (workspaceId, beforeId) => {
|
|
1283
|
+
setMenuFor(null)
|
|
1284
|
+
const insertBefore = ctx.workspaces?.insertBefore
|
|
1285
|
+
if (typeof insertBefore !== 'function') return
|
|
1286
|
+
run(insertBefore.call(ctx.workspaces, workspaceId, beforeId), 'notice.moveFailed')
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// ----- rename input -----
|
|
1290
|
+
const renameInput = (draft) => h('input', {
|
|
1291
|
+
key: 'rename',
|
|
1292
|
+
className: 'dfs-input',
|
|
1293
|
+
value: draft.value,
|
|
1294
|
+
autoFocus: true,
|
|
1295
|
+
onClick: (event) => event.stopPropagation(),
|
|
1296
|
+
onContextMenu: (event) => event.stopPropagation(),
|
|
1297
|
+
onChange: (event) => setRename({ ...draft, value: event.target.value }),
|
|
1298
|
+
onKeyDown: (event) => {
|
|
1299
|
+
if (event.key === 'Enter') { event.preventDefault(); commitRename() }
|
|
1300
|
+
if (event.key === 'Escape') { event.preventDefault(); setRename(null) }
|
|
1301
|
+
},
|
|
1302
|
+
})
|
|
1303
|
+
|
|
1304
|
+
// ----- menu -----
|
|
1305
|
+
// Rendered once at the root as a fixed layer: a panel inside the
|
|
1306
|
+
// scrolling list would be clipped at the list's edge.
|
|
1307
|
+
const menuEl = menuFor === null ? null : h('div', {
|
|
1308
|
+
className: 'dfs-menu',
|
|
1309
|
+
'data-dfs-menu': '1',
|
|
1310
|
+
role: 'menu',
|
|
1311
|
+
style: { left: menuFor.left, top: menuFor.top },
|
|
1312
|
+
}, menuFor.items.map((item) => (item.kind === 'divider'
|
|
1313
|
+
? h('div', { key: item.key, className: 'dfs-menuDivider', role: 'separator' })
|
|
1314
|
+
: item.kind === 'caption'
|
|
1315
|
+
? h('div', { key: item.key, className: 'dfs-menuCaption', role: 'presentation' }, item.label)
|
|
1316
|
+
: h('button', {
|
|
1317
|
+
key: item.key,
|
|
1318
|
+
type: 'button',
|
|
1319
|
+
role: item.check === undefined ? 'menuitem' : 'menuitemradio',
|
|
1320
|
+
'aria-checked': item.check === undefined ? undefined : item.check,
|
|
1321
|
+
className: `dfs-menuItem${item.danger === true ? ' is-danger' : ''}`,
|
|
1322
|
+
onClick: (event) => { event.stopPropagation(); item.onSelect() },
|
|
1323
|
+
},
|
|
1324
|
+
item.check === undefined
|
|
1325
|
+
? item.label
|
|
1326
|
+
: [
|
|
1327
|
+
h('span', { key: 'check', className: 'dfs-menuCheck', 'aria-hidden': true }, item.check ? '✓' : ''),
|
|
1328
|
+
h('span', { key: 'label', className: 'dfs-menuLabel' }, item.label),
|
|
1329
|
+
]))))
|
|
1330
|
+
|
|
1331
|
+
const noticeEl = () => h('div', { className: 'dfs-notice', role: 'status' },
|
|
1332
|
+
h('span', { className: 'dfs-noticeText' }, notice.message),
|
|
1333
|
+
notice.action === null ? null : h('button', {
|
|
1334
|
+
key: 'action',
|
|
1335
|
+
type: 'button',
|
|
1336
|
+
className: 'dfs-noticeAction',
|
|
1337
|
+
onClick: () => { notice.action.onSelect(); setNotice(null) },
|
|
1338
|
+
}, notice.action.label))
|
|
1339
|
+
|
|
1340
|
+
const hoverEl = hoverFor === null ? null : h('div', {
|
|
1341
|
+
className: 'dfs-hover',
|
|
1342
|
+
role: 'tooltip',
|
|
1343
|
+
style: { left: hoverFor.left, top: hoverFor.top },
|
|
1344
|
+
}, hoverFor.rows.map((row) => h('div', { key: row.key, className: 'dfs-hoverRow' },
|
|
1345
|
+
row.kind === undefined
|
|
1346
|
+
? h('span', { className: 'dfs-hoverSpacer' })
|
|
1347
|
+
: h(Indicator, { kind: row.kind, title: row.label }),
|
|
1348
|
+
h('span', { className: 'dfs-hoverLabel' }, row.label))))
|
|
1349
|
+
|
|
1350
|
+
/** Place the menu with the pointer or a trigger rect, kept in the viewport. */
|
|
1351
|
+
const showMenu = (key, items, x, y) => {
|
|
1352
|
+
cancelHover()
|
|
1353
|
+
const width = 168
|
|
1354
|
+
const height = items.length * 26 + 10
|
|
1355
|
+
const viewportW = typeof window === 'undefined' ? 1280 : window.innerWidth
|
|
1356
|
+
const viewportH = typeof window === 'undefined' ? 800 : window.innerHeight
|
|
1357
|
+
setMenuFor({
|
|
1358
|
+
key,
|
|
1359
|
+
items,
|
|
1360
|
+
left: Math.max(8, Math.min(x, viewportW - width - 8)),
|
|
1361
|
+
top: Math.max(8, Math.min(y, viewportH - height - 8)),
|
|
1362
|
+
})
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
const toggleMenuAt = (key, items, event) => {
|
|
1366
|
+
event.stopPropagation()
|
|
1367
|
+
if (menuFor !== null && menuFor.key === key) { setMenuFor(null); return }
|
|
1368
|
+
const rect = event.currentTarget.getBoundingClientRect()
|
|
1369
|
+
showMenu(key, items, rect.right - 168, rect.bottom + 4)
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
const contextMenuHandler = (key, items) => (event) => {
|
|
1373
|
+
event.preventDefault()
|
|
1374
|
+
event.stopPropagation()
|
|
1375
|
+
showMenu(key, items, event.clientX, event.clientY)
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
const menuTrigger = (key, items) => h('button', {
|
|
1379
|
+
type: 'button',
|
|
1380
|
+
className: 'dfs-iconBtn',
|
|
1381
|
+
'data-dfs-menu-trigger': '1',
|
|
1382
|
+
title: t('action.menu'),
|
|
1383
|
+
'aria-label': t('action.menu'),
|
|
1384
|
+
'aria-expanded': menuFor !== null && menuFor.key === key,
|
|
1385
|
+
onClick: (event) => toggleMenuAt(key, items, event),
|
|
1386
|
+
}, DotsIcon())
|
|
1387
|
+
|
|
1388
|
+
if (!wide) {
|
|
1389
|
+
return h('div', { className: 'dfs-rail' },
|
|
1390
|
+
h('button', {
|
|
1391
|
+
type: 'button', className: 'dfs-iconBtn', title: t('rail.expand'),
|
|
1392
|
+
'aria-label': t('rail.expand'),
|
|
1393
|
+
onClick: () => { if (typeof expandSidebar === 'function') expandSidebar() },
|
|
1394
|
+
}, FolderIcon()))
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const body = [h('style', { key: 'style' }, CSS)]
|
|
1398
|
+
|
|
1399
|
+
// ----- search results replace both levels while a query exists -----
|
|
1400
|
+
const text = query.trim()
|
|
1401
|
+
if (text !== '') {
|
|
1402
|
+
const lower = text.toLowerCase()
|
|
1403
|
+
const byId = list === undefined || list === null ? undefined : list.byId
|
|
1404
|
+
const seen = new Set()
|
|
1405
|
+
const hits = []
|
|
1406
|
+
if (byId !== undefined && byId !== null) {
|
|
1407
|
+
for (const id of Object.keys(byId)) {
|
|
1408
|
+
const summary = byId[id]
|
|
1409
|
+
if (summary === null || typeof summary !== 'object') continue
|
|
1410
|
+
const isArchivedHit = archived.has(id)
|
|
1411
|
+
if (archivedFilter === 'hide' && isArchivedHit) continue
|
|
1412
|
+
if (archivedFilter === 'only' && !isArchivedHit) continue
|
|
1413
|
+
const workspace = workspaceOf(id) ?? ungroupedWorkspace
|
|
1414
|
+
const title = titleOf(summary)
|
|
1415
|
+
if (!title.toLowerCase().includes(lower)) continue
|
|
1416
|
+
seen.add(id)
|
|
1417
|
+
hits.push({ sessionId: id, title, workspace, snippet: undefined })
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
for (const item of contentHits) {
|
|
1421
|
+
if (!seen.has(item.sessionId)) {
|
|
1422
|
+
const summary = byId === undefined || byId === null ? undefined : byId[item.sessionId]
|
|
1423
|
+
const workspace = workspaceOf(item.sessionId) ?? ungroupedWorkspace
|
|
1424
|
+
const isArchivedHit = archived.has(item.sessionId)
|
|
1425
|
+
if (summary === undefined) continue
|
|
1426
|
+
if (archivedFilter === 'hide' && isArchivedHit) continue
|
|
1427
|
+
if (archivedFilter === 'only' && !isArchivedHit) continue
|
|
1428
|
+
seen.add(item.sessionId)
|
|
1429
|
+
hits.push({ sessionId: item.sessionId, title: titleOf(summary), workspace, snippet: item.snippet })
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
const resultRows = hits.slice(0, 30).map((hit) => h('div', { key: hit.sessionId, className: 'dfs-rowWrap' },
|
|
1433
|
+
h('div', {
|
|
1434
|
+
className: 'dfs-row', role: 'button', tabIndex: 0,
|
|
1435
|
+
onMouseEnter: startMarquee,
|
|
1436
|
+
onMouseLeave: stopMarquee,
|
|
1437
|
+
onClick: () => openSession(hit.sessionId),
|
|
1438
|
+
onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openSession(hit.sessionId) } },
|
|
1439
|
+
},
|
|
1440
|
+
h(Title, { text: hit.title }),
|
|
1441
|
+
h('span', { className: 'dfs-time' }, baseName(hit.workspace.path) || t('group.ungrouped')),
|
|
1442
|
+
hit.snippet === undefined ? null : h('span', { className: 'dfs-snippet' }, `${t('search.content')}: ${hit.snippet}`))))
|
|
1443
|
+
body.push(topBar())
|
|
1444
|
+
body.push(h('div', { key: 'list', className: 'dfs-list' },
|
|
1445
|
+
resultRows.length === 0 ? [h('div', { key: 'empty', className: 'dfs-empty' }, t('empty.search'))] : resultRows))
|
|
1446
|
+
return h('div', { className: 'dfs-root' }, body)
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/** Shared top bar for both levels: optional back, title, actions. */
|
|
1450
|
+
function headerBar(key, options) {
|
|
1451
|
+
const parts = []
|
|
1452
|
+
if (options.back === true) {
|
|
1453
|
+
parts.push(h('button', {
|
|
1454
|
+
key: 'back',
|
|
1455
|
+
type: 'button',
|
|
1456
|
+
className: 'dfs-iconBtn dfs-headBack',
|
|
1457
|
+
title: t('action.back'),
|
|
1458
|
+
'aria-label': t('action.back'),
|
|
1459
|
+
onClick: () => { setOpenId(null); setQuery('') },
|
|
1460
|
+
}, h(BackIcon)))
|
|
1461
|
+
}
|
|
1462
|
+
parts.push(options.titleNode ?? h('span', { key: 'title', className: 'dfs-headTitle', title: options.title }, options.title))
|
|
1463
|
+
for (const action of options.actions ?? []) parts.push(action)
|
|
1464
|
+
return h('div', { key, className: 'dfs-head' }, parts)
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/** The search bar that overlays the header while expanded. */
|
|
1468
|
+
function searchBar(key) {
|
|
1469
|
+
return h('div', { key, className: 'dfs-search dfs-searchHeader', 'aria-hidden': searchOpen ? undefined : 'true' },
|
|
1470
|
+
h(SearchIcon, null),
|
|
1471
|
+
h('input', {
|
|
1472
|
+
ref: searchInput,
|
|
1473
|
+
tabIndex: searchOpen ? 0 : -1,
|
|
1474
|
+
value: query,
|
|
1475
|
+
placeholder: t('search.placeholder'),
|
|
1476
|
+
'aria-label': t('action.search'),
|
|
1477
|
+
onChange: (event) => setQuery(event.target.value),
|
|
1478
|
+
onKeyDown: (event) => { if (event.key === 'Escape') closeSearch() },
|
|
1479
|
+
}),
|
|
1480
|
+
h('button', {
|
|
1481
|
+
key: 'close',
|
|
1482
|
+
type: 'button',
|
|
1483
|
+
tabIndex: searchOpen ? 0 : -1,
|
|
1484
|
+
className: 'dfs-iconBtn',
|
|
1485
|
+
title: t('action.close'),
|
|
1486
|
+
'aria-label': t('action.close'),
|
|
1487
|
+
onClick: () => closeSearch(),
|
|
1488
|
+
}, h(CloseIcon)))
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
const closeSearch = () => {
|
|
1492
|
+
setSearchOpen(false)
|
|
1493
|
+
setQuery('')
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
React.useEffect(() => {
|
|
1497
|
+
if (!searchOpen) return undefined
|
|
1498
|
+
const node = searchInput.current
|
|
1499
|
+
if (node !== null && typeof node.focus === 'function') node.focus()
|
|
1500
|
+
return undefined
|
|
1501
|
+
}, [searchOpen])
|
|
1502
|
+
|
|
1503
|
+
/** The fixed top bar: identical on both levels, a home for actions. */
|
|
1504
|
+
function mainHeader(key) {
|
|
1505
|
+
return headerBar(key, {
|
|
1506
|
+
title: t('section.title'),
|
|
1507
|
+
actions: [
|
|
1508
|
+
h('button', {
|
|
1509
|
+
key: 'search',
|
|
1510
|
+
type: 'button',
|
|
1511
|
+
className: 'dfs-iconBtn',
|
|
1512
|
+
title: t('action.search'),
|
|
1513
|
+
'aria-label': t('action.search'),
|
|
1514
|
+
onClick: (event) => { event.stopPropagation(); setSearchOpen(true) },
|
|
1515
|
+
}, SearchIcon()),
|
|
1516
|
+
h('button', {
|
|
1517
|
+
key: 'view',
|
|
1518
|
+
type: 'button',
|
|
1519
|
+
className: 'dfs-iconBtn',
|
|
1520
|
+
title: `${t('action.view')} · ${t(`sort.${activeSort.mode}`)} ${activeSort.dir === 'desc' ? '↓' : '↑'} · ${t(`view.${archivedFilter}`)}`,
|
|
1521
|
+
'aria-label': t('action.view'),
|
|
1522
|
+
onClick: (event) => {
|
|
1523
|
+
event.stopPropagation()
|
|
1524
|
+
const rect = event.currentTarget.getBoundingClientRect()
|
|
1525
|
+
showMenu('view', viewMenuItems(), rect.right - 168, rect.bottom + 4)
|
|
1526
|
+
},
|
|
1527
|
+
}, h(SlidersIcon)),
|
|
1528
|
+
h('button', {
|
|
1529
|
+
key: 'add',
|
|
1530
|
+
type: 'button', className: 'dfs-iconBtn', title: t('action.addWorkspace'),
|
|
1531
|
+
'aria-label': t('action.addWorkspace'), onClick: addWorkspace,
|
|
1532
|
+
}, h(ProjectAddIcon)),
|
|
1533
|
+
],
|
|
1534
|
+
})
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
/** One top bar: header and search overlay share a row so the switch animates. */
|
|
1538
|
+
function topBar() {
|
|
1539
|
+
return h('div', { key: 'topbar', className: 'dfs-topbar' + (searchOpen ? ' is-search' : '') },
|
|
1540
|
+
mainHeader('head'),
|
|
1541
|
+
searchBar('searchbar'))
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
const unarchive = (sessionId) => {
|
|
1545
|
+
setMenuFor(null)
|
|
1546
|
+
const call = ctx.uiWorkspace?.unarchiveSession
|
|
1547
|
+
if (typeof call !== 'function') return
|
|
1548
|
+
Promise.resolve(call.call(ctx.uiWorkspace, sessionId))
|
|
1549
|
+
.catch((error) => reportFailure(error, 'notice.unarchiveFailed'))
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
const applyArchivedFilter = (value) => {
|
|
1553
|
+
setArchivedFilter(value)
|
|
1554
|
+
writeArchivedFilter(value)
|
|
1555
|
+
setMenuFor(null)
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/** One view-options menu: ordering for the level in view, then visibility. */
|
|
1559
|
+
const viewMenuItems = () => ([
|
|
1560
|
+
{ key: 'caption:sort', kind: 'caption', label: `${t('action.sort')} · ${activeLevel === 'folders' ? t('level.folders') : t('level.sessions')}` },
|
|
1561
|
+
...modeItems(),
|
|
1562
|
+
...dirItems(),
|
|
1563
|
+
{ key: 'divider', kind: 'divider' },
|
|
1564
|
+
{ key: 'caption:view', kind: 'caption', label: t('action.view') },
|
|
1565
|
+
...ARCHIVED_FILTERS.map((value) => ({
|
|
1566
|
+
key: `view:${value}`,
|
|
1567
|
+
label: t(`view.${value}`),
|
|
1568
|
+
check: value === archivedFilter,
|
|
1569
|
+
onSelect: () => applyArchivedFilter(value),
|
|
1570
|
+
})),
|
|
1571
|
+
])
|
|
1572
|
+
|
|
1573
|
+
const openWorkspace = openId === null
|
|
1574
|
+
? undefined
|
|
1575
|
+
: (openId === UNGROUPED
|
|
1576
|
+
? ungroupedWorkspace
|
|
1577
|
+
: workspaces.find((workspace) => workspace.workspaceId === openId))
|
|
1578
|
+
|
|
1579
|
+
if (openWorkspace === undefined) {
|
|
1580
|
+
const listRows = []
|
|
1581
|
+
orderedWorkspaces.forEach((workspace) => {
|
|
1582
|
+
const rows = rowsFor(workspace)
|
|
1583
|
+
if (archivedFilter === 'only' && rows.length === 0) return
|
|
1584
|
+
const name = displayNameOf(workspace)
|
|
1585
|
+
const isCurrent = workspace.workspaceId === currentWorkspaceId
|
|
1586
|
+
const renaming = rename !== null && rename.kind === 'workspace' && rename.id === workspace.workspaceId
|
|
1587
|
+
const menuKey = `workspace:${workspace.workspaceId}`
|
|
1588
|
+
const folderKind = folderKindOf(workspace)
|
|
1589
|
+
const hostIndex = workspaces.indexOf(workspace)
|
|
1590
|
+
const moveItems = []
|
|
1591
|
+
if (folderSort.mode === 'manual' && folderSort.dir === 'asc') {
|
|
1592
|
+
if (hostIndex > 0) {
|
|
1593
|
+
moveItems.push({
|
|
1594
|
+
key: 'up',
|
|
1595
|
+
label: t('action.moveUp'),
|
|
1596
|
+
onSelect: () => moveWorkspace(workspace.workspaceId, workspaces[hostIndex - 1].workspaceId),
|
|
1597
|
+
})
|
|
1598
|
+
}
|
|
1599
|
+
if (hostIndex < workspaces.length - 1) {
|
|
1600
|
+
const following = hostIndex + 2 < workspaces.length ? workspaces[hostIndex + 2].workspaceId : undefined
|
|
1601
|
+
moveItems.push({
|
|
1602
|
+
key: 'down',
|
|
1603
|
+
label: t('action.moveDown'),
|
|
1604
|
+
onSelect: () => moveWorkspace(workspace.workspaceId, following),
|
|
1605
|
+
})
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
const folderMenu = [
|
|
1609
|
+
{ key: 'rename', label: t('action.renameWorkspace'), onSelect: () => startRenameWorkspace(workspace) },
|
|
1610
|
+
{ key: 'new', label: t('action.newSessionHere'), onSelect: () => startSession(workspace.workspaceId) },
|
|
1611
|
+
...moveItems,
|
|
1612
|
+
{ key: 'delete', label: t('action.deleteWorkspace'), danger: true, onSelect: () => { setMenuFor(null); setConfirm({ kind: 'deleteWorkspace', workspaceId: workspace.workspaceId }) } },
|
|
1613
|
+
]
|
|
1614
|
+
listRows.push(h('div', {
|
|
1615
|
+
key: workspace.workspaceId,
|
|
1616
|
+
className: `dfs-rowWrap${dropEdge !== null && dropEdge.id === workspace.workspaceId ? ` is-drop-${dropEdge.edge}` : ''}`,
|
|
1617
|
+
draggable: canReorder,
|
|
1618
|
+
onDragStart: (event) => {
|
|
1619
|
+
if (!canReorder) return
|
|
1620
|
+
const target = event.target
|
|
1621
|
+
if (target !== null && typeof target.closest === 'function' && target.closest('button') !== null) {
|
|
1622
|
+
event.preventDefault()
|
|
1623
|
+
return
|
|
1624
|
+
}
|
|
1625
|
+
setDragId(workspace.workspaceId)
|
|
1626
|
+
try {
|
|
1627
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
1628
|
+
event.dataTransfer.setData('text/plain', workspace.workspaceId)
|
|
1629
|
+
} catch {
|
|
1630
|
+
// Some environments expose a read-only dataTransfer.
|
|
1631
|
+
}
|
|
1632
|
+
},
|
|
1633
|
+
onDragOver: (event) => {
|
|
1634
|
+
if (!canReorder || dragId === null || dragId === workspace.workspaceId) return
|
|
1635
|
+
event.preventDefault()
|
|
1636
|
+
const rect = event.currentTarget.getBoundingClientRect()
|
|
1637
|
+
const edge = event.clientY < rect.top + rect.height / 2 ? 'before' : 'after'
|
|
1638
|
+
setDropEdge((previous) => (previous !== null && previous.id === workspace.workspaceId && previous.edge === edge
|
|
1639
|
+
? previous
|
|
1640
|
+
: { id: workspace.workspaceId, edge }))
|
|
1641
|
+
},
|
|
1642
|
+
onDragLeave: (event) => {
|
|
1643
|
+
if (event.currentTarget.contains(event.relatedTarget)) return
|
|
1644
|
+
setDropEdge((previous) => (previous !== null && previous.id === workspace.workspaceId ? null : previous))
|
|
1645
|
+
},
|
|
1646
|
+
onDrop: (event) => {
|
|
1647
|
+
if (!canReorder) return
|
|
1648
|
+
event.preventDefault()
|
|
1649
|
+
const edge = dropEdge !== null && dropEdge.id === workspace.workspaceId ? dropEdge.edge : 'before'
|
|
1650
|
+
dropOn(workspace.workspaceId, edge)
|
|
1651
|
+
},
|
|
1652
|
+
onDragEnd: clearDrag,
|
|
1653
|
+
},
|
|
1654
|
+
h('div', {
|
|
1655
|
+
className: `dfs-row${isCurrent ? ' is-current' : ''}`,
|
|
1656
|
+
role: 'button',
|
|
1657
|
+
tabIndex: 0,
|
|
1658
|
+
title: typeof workspace.path === 'string' && workspace.path !== '' ? workspace.path : name,
|
|
1659
|
+
onClick: () => { if (!renaming) setOpenId(workspace.workspaceId) },
|
|
1660
|
+
onContextMenu: contextMenuHandler(menuKey, folderMenu),
|
|
1661
|
+
onMouseEnter: (event) => {
|
|
1662
|
+
startMarquee(event)
|
|
1663
|
+
scheduleHover(menuKey, folderStatusRows(workspace), event)
|
|
1664
|
+
},
|
|
1665
|
+
onMouseLeave: (event) => { stopMarquee(event); cancelHover() },
|
|
1666
|
+
onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); setOpenId(workspace.workspaceId) } },
|
|
1667
|
+
},
|
|
1668
|
+
h(FolderGlyph, { kind: folderKind, label: folderKind === undefined ? undefined : labels[folderKind] }),
|
|
1669
|
+
renaming
|
|
1670
|
+
? renameInput(rename)
|
|
1671
|
+
: h(Title, { text: name === '' ? t('section.title') : name }),
|
|
1672
|
+
h('span', { className: `dfs-rowActions${menuFor !== null && menuFor.key === menuKey ? ' is-open' : ''}` },
|
|
1673
|
+
menuTrigger(menuKey, folderMenu)),
|
|
1674
|
+
rows.length > 0 ? h('span', { className: 'dfs-badge' }, String(rows.length)) : null,
|
|
1675
|
+
h(ChevronIcon, null)),
|
|
1676
|
+
confirm !== null && confirm.kind === 'deleteWorkspace' && confirm.workspaceId === workspace.workspaceId
|
|
1677
|
+
? h('div', { className: 'dfs-confirm' },
|
|
1678
|
+
h('span', { className: 'dfs-confirmText' }, t('confirm.deleteWorkspace')),
|
|
1679
|
+
h('button', { type: 'button', className: 'dfs-miniBtn is-danger', onClick: () => deleteWorkspace(workspace.workspaceId) }, t('confirm.yes')),
|
|
1680
|
+
h('button', { type: 'button', className: 'dfs-miniBtn', onClick: () => setConfirm(null) }, t('confirm.cancel')))
|
|
1681
|
+
: null))
|
|
1682
|
+
})
|
|
1683
|
+
const ungroupedRows = rowsFor(ungroupedWorkspace)
|
|
1684
|
+
if (ungroupedRows.length > 0) {
|
|
1685
|
+
const menuKey = 'workspace:' + UNGROUPED
|
|
1686
|
+
listRows.push(h('div', { key: UNGROUPED, className: 'dfs-rowWrap' },
|
|
1687
|
+
h('div', {
|
|
1688
|
+
className: `dfs-row${openId === UNGROUPED ? ' is-current' : ''}`,
|
|
1689
|
+
role: 'button',
|
|
1690
|
+
tabIndex: 0,
|
|
1691
|
+
title: t('group.ungrouped'),
|
|
1692
|
+
onClick: () => setOpenId(UNGROUPED),
|
|
1693
|
+
onMouseEnter: (event) => {
|
|
1694
|
+
startMarquee(event)
|
|
1695
|
+
scheduleHover(menuKey, folderStatusRows(ungroupedWorkspace), event)
|
|
1696
|
+
},
|
|
1697
|
+
onMouseLeave: (event) => { stopMarquee(event); cancelHover() },
|
|
1698
|
+
onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); setOpenId(UNGROUPED) } },
|
|
1699
|
+
},
|
|
1700
|
+
h(FolderGlyph, { kind: folderKindOf(ungroupedWorkspace), label: undefined }),
|
|
1701
|
+
h(Title, { text: t('group.ungrouped') }),
|
|
1702
|
+
h('span', { className: 'dfs-badge' }, String(ungroupedRows.length)),
|
|
1703
|
+
h(ChevronIcon, null))))
|
|
1704
|
+
}
|
|
1705
|
+
if (listRows.length === 0) {
|
|
1706
|
+
listRows.push(h('div', { key: 'empty', className: 'dfs-empty' }, t(archivedFilter === 'only' ? 'empty.archived' : 'empty.workspaces')))
|
|
1707
|
+
}
|
|
1708
|
+
body.push(topBar())
|
|
1709
|
+
body.push(h('div', { key: 'list', className: 'dfs-list' }, listRows))
|
|
1710
|
+
return h('div', { className: 'dfs-root' }, body, menuEl, hoverEl, notice === null ? null : noticeEl())
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// ----- one Workspace's Session list -----
|
|
1714
|
+
const isUngrouped = openWorkspace.workspaceId === UNGROUPED
|
|
1715
|
+
const name = isUngrouped
|
|
1716
|
+
? t('group.ungrouped')
|
|
1717
|
+
: (typeof openWorkspace.title === 'string' && openWorkspace.title !== ''
|
|
1718
|
+
? openWorkspace.title
|
|
1719
|
+
: baseName(openWorkspace.path))
|
|
1720
|
+
const workspaceRenaming = rename !== null && rename.kind === 'workspace' && rename.id === openWorkspace.workspaceId
|
|
1721
|
+
body.push(topBar())
|
|
1722
|
+
|
|
1723
|
+
// The folder row sits below the search field: the whole row goes back
|
|
1724
|
+
// to the folder list, the trailing plus starts a Session here.
|
|
1725
|
+
body.push(h('div', {
|
|
1726
|
+
key: 'crumb',
|
|
1727
|
+
className: 'dfs-crumb',
|
|
1728
|
+
role: 'button',
|
|
1729
|
+
tabIndex: 0,
|
|
1730
|
+
title: t('action.back'),
|
|
1731
|
+
onMouseEnter: startMarquee,
|
|
1732
|
+
onMouseLeave: stopMarquee,
|
|
1733
|
+
onClick: () => { setOpenId(null); setQuery('') },
|
|
1734
|
+
onKeyDown: (event) => {
|
|
1735
|
+
if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); setOpenId(null); setQuery('') }
|
|
1736
|
+
},
|
|
1737
|
+
},
|
|
1738
|
+
h('span', { className: 'dfs-crumbChevron', 'aria-hidden': true }, h(BackIcon)),
|
|
1739
|
+
h(FolderIcon, null),
|
|
1740
|
+
workspaceRenaming
|
|
1741
|
+
? renameInput(rename)
|
|
1742
|
+
: h(Title, { className: 'dfs-crumbName', text: name }),
|
|
1743
|
+
isUngrouped ? null : h('button', {
|
|
1744
|
+
key: 'new',
|
|
1745
|
+
type: 'button', className: 'dfs-iconBtn', title: t('action.newSession'),
|
|
1746
|
+
'aria-label': t('action.newSession'),
|
|
1747
|
+
onClick: (event) => { event.stopPropagation(); startSession(openWorkspace.workspaceId) },
|
|
1748
|
+
}, h(NewChatIcon))))
|
|
1749
|
+
|
|
1750
|
+
const rows = orderSessions(rowsFor(openWorkspace))
|
|
1751
|
+
const sessionRows = []
|
|
1752
|
+
for (const summary of rows) {
|
|
1753
|
+
const status = statusOf(summary.id)
|
|
1754
|
+
const kind = indicatorOf(status, projectionOf(summary.id))
|
|
1755
|
+
const label = kind === undefined ? undefined : (kind === 'waiting' ? waitingLabel(status) : labels[kind])
|
|
1756
|
+
const menuKey = `session:${summary.id}`
|
|
1757
|
+
const renaming = rename !== null && rename.kind === 'session' && rename.id === summary.id
|
|
1758
|
+
const isPinned = pinned.has(summary.id)
|
|
1759
|
+
const isArchived = archived.has(summary.id)
|
|
1760
|
+
const openRow = () => {
|
|
1761
|
+
if (isArchived) { notify(t('notice.archivedNotOpenable')); return }
|
|
1762
|
+
openSession(summary.id)
|
|
1763
|
+
}
|
|
1764
|
+
const sessionMenu = isArchived
|
|
1765
|
+
? [
|
|
1766
|
+
{ key: 'unarchive', label: t('action.unarchive'), onSelect: () => unarchive(summary.id) },
|
|
1767
|
+
{ key: 'rename', label: t('action.rename'), onSelect: () => startRenameSession(summary) },
|
|
1768
|
+
{ key: 'fork', label: t('action.fork'), onSelect: () => fork(summary.id) },
|
|
1769
|
+
{ key: 'copy', label: t('action.copyId'), onSelect: () => copyId(summary.id) },
|
|
1770
|
+
]
|
|
1771
|
+
: [
|
|
1772
|
+
{ key: 'pin', label: isPinned ? t('action.unpin') : t('action.pin'), onSelect: () => togglePin(summary.id) },
|
|
1773
|
+
{ key: 'rename', label: t('action.rename'), onSelect: () => startRenameSession(summary) },
|
|
1774
|
+
{ key: 'fork', label: t('action.fork'), onSelect: () => fork(summary.id) },
|
|
1775
|
+
{ key: 'copy', label: t('action.copyId'), onSelect: () => copyId(summary.id) },
|
|
1776
|
+
{ key: 'archive', label: t('action.archive'), danger: true, onSelect: () => archiveNow(summary.id, false) },
|
|
1777
|
+
]
|
|
1778
|
+
sessionRows.push(h('div', { key: summary.id, className: 'dfs-rowWrap' },
|
|
1779
|
+
h('div', {
|
|
1780
|
+
className: `dfs-row${summary.id === currentId ? ' is-current' : ''}${isArchived ? ' is-archived' : ''}`,
|
|
1781
|
+
role: 'button',
|
|
1782
|
+
tabIndex: 0,
|
|
1783
|
+
title: isArchived ? t('notice.archivedNotOpenable') : (kind === undefined ? titleOf(summary) : `${titleOf(summary)} — ${label}`),
|
|
1784
|
+
onClick: () => { if (!renaming) openRow() },
|
|
1785
|
+
onContextMenu: contextMenuHandler(menuKey, sessionMenu),
|
|
1786
|
+
onMouseEnter: (event) => {
|
|
1787
|
+
startMarquee(event)
|
|
1788
|
+
scheduleHover(menuKey, statusRowsOf(status, projectionOf(summary.id), summary.updatedAt), event)
|
|
1789
|
+
},
|
|
1790
|
+
onMouseLeave: (event) => { stopMarquee(event); cancelHover() },
|
|
1791
|
+
onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openRow() } },
|
|
1792
|
+
},
|
|
1793
|
+
h(Indicator, { kind, title: label }),
|
|
1794
|
+
renaming ? renameInput(rename) : h(Title, { text: titleOf(summary) }),
|
|
1795
|
+
isPinned ? h('span', { className: 'dfs-pin', title: t('action.pin'), 'aria-hidden': true }, h(PinIcon)) : null,
|
|
1796
|
+
h('span', { className: `dfs-rowActions${menuFor !== null && menuFor.key === menuKey ? ' is-open' : ''}` },
|
|
1797
|
+
menuTrigger(menuKey, sessionMenu)),
|
|
1798
|
+
h('span', { className: 'dfs-time' }, relativeTime(summary.updatedAt))),
|
|
1799
|
+
confirm !== null && confirm.kind === 'archive' && confirm.sessionId === summary.id
|
|
1800
|
+
? h('div', { className: 'dfs-confirm' },
|
|
1801
|
+
h('span', { className: 'dfs-confirmText' }, t('confirm.archive')),
|
|
1802
|
+
h('button', { type: 'button', className: 'dfs-miniBtn is-danger', onClick: () => archiveNow(summary.id, true) }, t('confirm.yes')),
|
|
1803
|
+
h('button', { type: 'button', className: 'dfs-miniBtn', onClick: () => setConfirm(null) }, t('confirm.cancel')))
|
|
1804
|
+
: null))
|
|
1805
|
+
}
|
|
1806
|
+
if (sessionRows.length === 0) sessionRows.push(h('div', { key: 'empty', className: 'dfs-empty' }, t('empty.sessions')))
|
|
1807
|
+
body.push(h('div', { key: 'list', className: 'dfs-list' }, sessionRows))
|
|
1808
|
+
return h('div', { className: 'dfs-root' }, body, menuEl, hoverEl, notice === null ? null : noticeEl())
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// A single slot refuses a second registration at the same priority and
|
|
1812
|
+
// renders the lowest priority, so this row must sit below the shipped
|
|
1813
|
+
// browser (priority 0) to replace it. The boundary keeps a render bug
|
|
1814
|
+
// from abdicating this entry back to the shipped browser: it renders the
|
|
1815
|
+
// message in place instead, which is what a reader can diagnose.
|
|
1816
|
+
ctx.slots.inject('sidebar.workspaces', () => ctx.slots.register({
|
|
1817
|
+
name: 'sidebar.workspaces',
|
|
1818
|
+
priority: -1,
|
|
1819
|
+
locale: NS,
|
|
1820
|
+
}, (props) => h(RenderBoundary, null, h(FolderSidebar, props))))
|
|
1821
|
+
},
|
|
1822
|
+
}
|
|
1823
|
+
},
|
|
1824
|
+
})
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
if (typeof module === 'object' && module !== null && module.exports !== undefined) {
|
|
1828
|
+
module.exports = {
|
|
1829
|
+
INDICATOR_PRIORITY, INDICATOR_LABEL_KEY, GLYPH_COLOR,
|
|
1830
|
+
SORT_MODES, SORT_DIRS, SORT_DEFAULT, SORT_NATURAL_DIR,
|
|
1831
|
+
ARCHIVED_FILTERS, UNGROUPED,
|
|
1832
|
+
directionSign, normalizeSort, readSortState, writeSortState,
|
|
1833
|
+
readArchivedFilter, writeArchivedFilter,
|
|
1834
|
+
sortWorkspaces, sortSessions, pickFolderKind, indicatorOf,
|
|
1835
|
+
relativeTime, baseName, marqueeShiftOf, marqueeDurationOf,
|
|
1836
|
+
}
|
|
1837
|
+
}
|