@ash-qw/dsh-theme-prts 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/lib/client.js +759 -129
- package/package.json +1 -1
- package/src/client/index.js +1 -2
- package/src/client/preferences.js +7 -1
- package/src/client/preview-markdown.js +26 -0
- package/src/client/session-lifeline.js +1 -1
- package/src/client/session-summary-preview.js +273 -39
- package/src/client/sidebar-control-adapter.js +465 -54
- package/src/client/theme-settings-workbench.js +29 -48
- package/src/styles/facility-vector.css +110 -21
- package/src/styles/prts.css +5 -5
- package/src/styles/refinement.css +1 -1
- package/src/styles/theme-settings-workbench.css +13 -32
|
@@ -20,6 +20,7 @@ const OWNED_ATTRIBUTES = [
|
|
|
20
20
|
'data-prts-session-time',
|
|
21
21
|
'data-prts-session-index',
|
|
22
22
|
'data-prts-session-state',
|
|
23
|
+
'data-prts-selection-target',
|
|
23
24
|
'data-prts-session-summary-header',
|
|
24
25
|
'data-prts-session-summary-label',
|
|
25
26
|
'data-prts-facility-face',
|
|
@@ -190,8 +191,23 @@ function ensureFacilitySpine(document, row, kind, next) {
|
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
|
|
193
|
-
|
|
194
|
+
function createRunningMarker(document) {
|
|
195
|
+
const svgNode = name => document.createElementNS('http://www.w3.org/2000/svg', name)
|
|
196
|
+
const marker = svgNode('g')
|
|
197
|
+
marker.setAttribute('data-prts-running-marker', '')
|
|
198
|
+
const letter = svgNode('text')
|
|
199
|
+
letter.setAttribute('x', '12')
|
|
200
|
+
letter.setAttribute('y', '16')
|
|
201
|
+
letter.textContent = 'A'
|
|
202
|
+
const ring = svgNode('circle')
|
|
203
|
+
for (const [key, value] of Object.entries({cx:12, cy:12, r:10, 'stroke-dasharray':'4.5 11.208'})) ring.setAttribute(key, value)
|
|
204
|
+
marker.append(letter, ring)
|
|
205
|
+
return marker
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function createSidebarControlAdapter({ document, window, sessions, workspaces, getSessionStatus, getUiWorkspace, getUiConversation, getSessions, getWorkspaces }) {
|
|
194
209
|
let previewPreferences = {}
|
|
210
|
+
let adapterActive = false, sourceRetryTimer
|
|
195
211
|
let observer
|
|
196
212
|
let resizeObserver
|
|
197
213
|
let resizeFrame
|
|
@@ -203,15 +219,124 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
203
219
|
let statusSourcesStarted = false
|
|
204
220
|
let summaryRoot
|
|
205
221
|
let summarySignature = ''
|
|
206
|
-
|
|
207
|
-
let
|
|
208
|
-
|
|
222
|
+
let heldPreviewState
|
|
223
|
+
let hoveredSummary, summaryLeaveTimer
|
|
224
|
+
let summaryDismissed = false, pointerLeftSummary = false
|
|
225
|
+
function resumeSummaryHover(event) {
|
|
226
|
+
if (!summaryDismissed) return
|
|
227
|
+
const trigger = event.target.closest?.('[data-prts-session-summary-trigger]')
|
|
228
|
+
if (!trigger || !summaryRoot?.contains(trigger)) { pointerLeftSummary = true; return }
|
|
229
|
+
if (!pointerLeftSummary) return
|
|
230
|
+
summaryDismissed = false
|
|
231
|
+
summaryRoot.removeAttribute('data-prts-summary-dismissed')
|
|
232
|
+
document.removeEventListener('pointermove', resumeSummaryHover)
|
|
233
|
+
keepSummaryHover(trigger.closest('[data-prts-session-summary-group]'))
|
|
234
|
+
}
|
|
235
|
+
function closeNavigationPreview() {
|
|
236
|
+
heldPreviewState = undefined
|
|
237
|
+
preview.stop()
|
|
238
|
+
clearSummaryHover()
|
|
239
|
+
summaryDismissed = true
|
|
240
|
+
pointerLeftSummary = false
|
|
241
|
+
summaryRoot?.setAttribute('data-prts-summary-dismissed', '')
|
|
242
|
+
for (const group of summaryRoot?.querySelectorAll('[data-prts-session-summary-group]') ?? []) group.removeAttribute('data-prts-preview-open')
|
|
243
|
+
document.addEventListener('pointermove', resumeSummaryHover)
|
|
244
|
+
}
|
|
245
|
+
function clearSummaryHover() {
|
|
246
|
+
window.clearTimeout(summaryLeaveTimer)
|
|
247
|
+
hoveredSummary?.removeAttribute('data-prts-summary-hover-open')
|
|
248
|
+
hoveredSummary?.removeAttribute('data-prts-summary-instant')
|
|
249
|
+
hoveredSummary = undefined
|
|
250
|
+
}
|
|
251
|
+
function keepSummaryHover(group) {
|
|
252
|
+
if (summaryDismissed) return
|
|
253
|
+
window.clearTimeout(summaryLeaveTimer)
|
|
254
|
+
if (!hoveredSummary) group.removeAttribute('data-prts-summary-instant')
|
|
255
|
+
if (hoveredSummary && hoveredSummary !== group) {
|
|
256
|
+
hoveredSummary.setAttribute('data-prts-summary-instant', '')
|
|
257
|
+
hoveredSummary.removeAttribute('data-prts-summary-hover-open')
|
|
258
|
+
group.setAttribute('data-prts-summary-instant', '')
|
|
259
|
+
}
|
|
260
|
+
hoveredSummary = group
|
|
261
|
+
group.setAttribute('data-prts-summary-hover-open', '')
|
|
262
|
+
}
|
|
263
|
+
function dismissHeldPreview(event) {
|
|
264
|
+
if (!heldPreviewState || (event?.type === 'keydown' && event.key !== 'Escape')) return
|
|
265
|
+
if (event?.type === 'pointerdown' && (summaryRoot?.contains(event.target) || event.target.closest?.('[data-prts-summary-reply]'))) return
|
|
266
|
+
heldPreviewState = undefined
|
|
267
|
+
queueScan()
|
|
268
|
+
}
|
|
269
|
+
function enterPreviewState(state) {
|
|
270
|
+
if (heldPreviewState && heldPreviewState !== state) { heldPreviewState = undefined; queueScan() }
|
|
271
|
+
preview.enterState(state)
|
|
272
|
+
}
|
|
273
|
+
const preview = createSessionSummaryPreview({ document, window, sessions, getUiConversation, getSessions, getSessionStatus, openSession, getPreviewPreferences: () => previewPreferences, onPreviewClosed: () => { if (statusSourcesStarted) queueScan() }, onAnswerSubmitted: state => { heldPreviewState = state; queueScan() } })
|
|
274
|
+
const stopTitles = new Map()
|
|
275
|
+
const summaryGroups = new Map()
|
|
276
|
+
function clearSummaryEffects() { clearSummaryHover(); preview.stop(false); for (const entry of summaryGroups.values()) entry.dispose(); for (const stop of stopTitles.values()) stop(); stopTitles.clear(); summaryGroups.clear() }
|
|
209
277
|
let owned = new Set()
|
|
210
278
|
let vectorOwners = new Set()
|
|
211
279
|
const dirtyVectors = new Map()
|
|
212
280
|
const vectorSizes = new WeakMap()
|
|
213
281
|
const lifelineAnimator = createSessionLifelineAnimator({ document, window })
|
|
214
282
|
|
|
283
|
+
let subscribedSessions, subscribedWorkspaces
|
|
284
|
+
function disconnectSummarySources() {
|
|
285
|
+
statusSourcesStarted = false
|
|
286
|
+
window.clearTimeout(sourceRetryTimer)
|
|
287
|
+
sourceRetryTimer = undefined
|
|
288
|
+
stopSessionList()
|
|
289
|
+
stopWorkspaceList()
|
|
290
|
+
stopSessionStatus()
|
|
291
|
+
stopSessionList = stopWorkspaceList = stopSessionStatus = () => {}
|
|
292
|
+
subscribedSessions = subscribedWorkspaces = sessionStatus = undefined
|
|
293
|
+
if (getSessions) sessions = undefined
|
|
294
|
+
if (getWorkspaces) workspaces = undefined
|
|
295
|
+
cancelReveal()
|
|
296
|
+
cancelPendingNavigation()
|
|
297
|
+
clearSelectionTarget()
|
|
298
|
+
heldPreviewState = undefined
|
|
299
|
+
summaryDismissed = false
|
|
300
|
+
document?.removeEventListener('pointermove', resumeSummaryHover)
|
|
301
|
+
document?.removeEventListener('pointerdown', dismissHeldPreview)
|
|
302
|
+
document?.removeEventListener('keydown', dismissHeldPreview)
|
|
303
|
+
preview.stop()
|
|
304
|
+
}
|
|
305
|
+
function syncSummarySources() {
|
|
306
|
+
if (previewPreferences.sessionPreview === false) {
|
|
307
|
+
if (statusSourcesStarted) disconnectSummarySources()
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
if (!statusSourcesStarted) {
|
|
311
|
+
statusSourcesStarted = true
|
|
312
|
+
document.addEventListener('pointerdown', dismissHeldPreview)
|
|
313
|
+
document.addEventListener('keydown', dismissHeldPreview)
|
|
314
|
+
}
|
|
315
|
+
connectSummarySources()
|
|
316
|
+
connectSessionStatus()
|
|
317
|
+
const waiting = (getSessions && !subscribedSessions) || (getWorkspaces && !subscribedWorkspaces)
|
|
318
|
+
|| (getSessionStatus && !sessionStatus)
|
|
319
|
+
if (waiting && sourceRetryTimer === undefined) {
|
|
320
|
+
sourceRetryTimer = window.setTimeout(() => { sourceRetryTimer = undefined; queueScan() }, 250)
|
|
321
|
+
} else if (!waiting) {
|
|
322
|
+
window.clearTimeout(sourceRetryTimer)
|
|
323
|
+
sourceRetryTimer = undefined
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function connectSummarySources() {
|
|
327
|
+
if (getSessions) sessions = getSessions()
|
|
328
|
+
if (getWorkspaces) workspaces = getWorkspaces()
|
|
329
|
+
if (subscribedSessions !== sessions?.list) {
|
|
330
|
+
stopSessionList()
|
|
331
|
+
subscribedSessions = sessions?.list
|
|
332
|
+
stopSessionList = subscribedSessions?.subscribe?.(queueScan) ?? (() => {})
|
|
333
|
+
}
|
|
334
|
+
if (subscribedWorkspaces !== workspaces?.list) {
|
|
335
|
+
stopWorkspaceList()
|
|
336
|
+
subscribedWorkspaces = workspaces?.list
|
|
337
|
+
stopWorkspaceList = subscribedWorkspaces?.subscribe?.(queueScan) ?? (() => {})
|
|
338
|
+
}
|
|
339
|
+
}
|
|
215
340
|
function connectSessionStatus() {
|
|
216
341
|
const source = getSessionStatus?.()
|
|
217
342
|
if (source === sessionStatus) return
|
|
@@ -220,28 +345,139 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
220
345
|
stopSessionStatus = source?.subscribe?.(queueScan) ?? (() => {})
|
|
221
346
|
}
|
|
222
347
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
348
|
+
let selectionTarget, selectionTargetTimer
|
|
349
|
+
function clearSelectionTarget() {
|
|
350
|
+
selectionTarget = undefined
|
|
351
|
+
window.clearTimeout(selectionTargetTimer)
|
|
352
|
+
document?.removeEventListener('pointerdown', interruptSelectionTarget, true)
|
|
353
|
+
queueScan()
|
|
354
|
+
}
|
|
355
|
+
function interruptSelectionTarget(event) {
|
|
356
|
+
if (event.target.closest?.('[data-prts-session-row]')) clearSelectionTarget()
|
|
357
|
+
}
|
|
358
|
+
function setSelectionTarget(id) {
|
|
359
|
+
clearSelectionTarget()
|
|
360
|
+
selectionTarget = id
|
|
361
|
+
selectionTargetTimer = window.setTimeout(clearSelectionTarget, 4000)
|
|
362
|
+
document.addEventListener('pointerdown', interruptSelectionTarget, true)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let pendingReveal, revealTimer, revealFrame, revealSettleTimer
|
|
366
|
+
let navigationFrame, navigationGeneration = 0
|
|
367
|
+
function cancelPendingNavigation() {
|
|
368
|
+
navigationGeneration++
|
|
369
|
+
window.cancelAnimationFrame?.(navigationFrame)
|
|
370
|
+
}
|
|
371
|
+
function cancelReveal() {
|
|
372
|
+
pendingReveal = undefined
|
|
373
|
+
window.clearTimeout(revealTimer)
|
|
374
|
+
window.clearTimeout(revealSettleTimer)
|
|
375
|
+
window.cancelAnimationFrame?.(revealFrame)
|
|
376
|
+
}
|
|
377
|
+
function revealSessionCard(sessionId) {
|
|
378
|
+
cancelReveal()
|
|
379
|
+
pendingReveal = {sessionId, attempts: 0}
|
|
380
|
+
revealTimer = window.setTimeout(cancelReveal, 4000)
|
|
381
|
+
const root = document?.querySelector?.('[data-prts-region="sessions"] [data-slot="sidebar.workspaces"]')
|
|
382
|
+
syncSessionReveal(root)
|
|
383
|
+
queueScan()
|
|
384
|
+
return Boolean(pendingReveal?.expanding)
|
|
385
|
+
}
|
|
386
|
+
function syncSessionReveal(root) {
|
|
387
|
+
if (!pendingReveal || !root) return
|
|
388
|
+
const {sessionId} = pendingReveal
|
|
389
|
+
const rows = Array.from(root.querySelectorAll('[data-row-key], [data-session-id]'))
|
|
390
|
+
const row = rows.find(el => el.getAttribute('data-row-key') === `session:${sessionId}`
|
|
391
|
+
|| el.getAttribute('data-session-id') === sessionId)
|
|
392
|
+
if (row) {
|
|
393
|
+
if (pendingReveal.scrolling) return
|
|
394
|
+
const request = pendingReveal
|
|
395
|
+
request.scrolling = true
|
|
396
|
+
// Let React finish mounting and let the host's row glide settle before scrolling.
|
|
397
|
+
revealFrame = window.requestAnimationFrame(async () => {
|
|
398
|
+
const reduced = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
399
|
+
const animations = []
|
|
400
|
+
if (!reduced) for (let node = row; node && node !== root; node = node.parentElement) {
|
|
401
|
+
for (const animation of node.getAnimations?.() ?? []) {
|
|
402
|
+
const timing = animation.effect?.getComputedTiming?.()
|
|
403
|
+
if (animation.playState === 'running' && Number.isFinite(timing?.endTime)) animations.push(animation.finished)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (animations.length) await Promise.race([
|
|
407
|
+
Promise.allSettled(animations),
|
|
408
|
+
new Promise(resolve => { revealSettleTimer = window.setTimeout(resolve, 300) }),
|
|
409
|
+
])
|
|
410
|
+
if (pendingReveal !== request || !row.isConnected) return
|
|
411
|
+
row.scrollIntoView?.({block:'nearest', inline:'nearest', behavior:reduced ? 'instant' : 'smooth'})
|
|
412
|
+
cancelReveal()
|
|
413
|
+
})
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
if (++pendingReveal.attempts > 40) { cancelReveal(); return }
|
|
417
|
+
const items = snapshotOf(workspaces?.list)?.items ?? []
|
|
418
|
+
const owner = items.find(item => item.sessionIds?.includes(sessionId))
|
|
419
|
+
const key = owner?.workspaceId ?? ''
|
|
420
|
+
const keyed = key => rows.find(el => el.getAttribute('data-row-key') === key)
|
|
421
|
+
const group = keyed(`workspace:${key}`)
|
|
422
|
+
if (group?.getAttribute('aria-expanded') === 'false') { pendingReveal.expanding = true; group.click(); return }
|
|
423
|
+
if (!group && owner?.path) {
|
|
424
|
+
// Nested workspaces may not mount until their visible ancestor is expanded.
|
|
425
|
+
const path = value => value?.replace(/\\/g, '/').replace(/\/$/, '')
|
|
426
|
+
const target = path(owner.path)
|
|
427
|
+
const ancestors = items.filter(item => item !== owner && item.path && target.startsWith(path(item.path) + '/'))
|
|
428
|
+
.sort((a,b) => b.path.length - a.path.length)
|
|
429
|
+
for (const ancestor of ancestors) {
|
|
430
|
+
const parent = keyed(`workspace:${ancestor.workspaceId}`)
|
|
431
|
+
if (parent?.getAttribute('aria-expanded') === 'false') { pendingReveal.expanding = true; parent.click(); return }
|
|
233
432
|
}
|
|
234
|
-
}
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
433
|
+
}
|
|
434
|
+
const overflow = keyed(`overflow:${key}`)
|
|
435
|
+
if (overflow?.getAttribute('aria-expanded') === 'false') { pendingReveal.expanding = true; overflow.click() }
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function openSession(sessionId) {
|
|
439
|
+
closeNavigationPreview()
|
|
440
|
+
cancelPendingNavigation()
|
|
441
|
+
setSelectionTarget(sessionId)
|
|
442
|
+
revealSessionCard(sessionId)
|
|
443
|
+
const generation = navigationGeneration
|
|
444
|
+
const navigate = () => {
|
|
445
|
+
if (generation !== navigationGeneration || !adapterActive) return
|
|
446
|
+
try {
|
|
447
|
+
const uiWorkspace = getUiWorkspace?.()
|
|
448
|
+
if (typeof uiWorkspace?.openSession === 'function') {
|
|
449
|
+
uiWorkspace.openSession(sessionId)
|
|
450
|
+
return
|
|
451
|
+
}
|
|
452
|
+
if (typeof sessions?.open === 'function') {
|
|
453
|
+
sessions.open(sessionId)
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
} catch {}
|
|
457
|
+
const row = Array.from(document.querySelectorAll('[data-prts-session-row]')).find(candidate => (
|
|
458
|
+
candidate.getAttribute('data-row-key') === `session:${sessionId}`
|
|
459
|
+
|| candidate.getAttribute('data-session-id') === sessionId
|
|
460
|
+
|| candidate.querySelector('[data-prts-row-title]')?.textContent.trim() === sessionId
|
|
461
|
+
))
|
|
462
|
+
if (row) row.click?.()
|
|
463
|
+
else clearSelectionTarget()
|
|
464
|
+
}
|
|
465
|
+
// Paint selection and expansion before the host synchronously mounts another conversation.
|
|
466
|
+
// Already-expanded workspaces need this too: queueScan has not applied selection yet.
|
|
467
|
+
navigationFrame = window.requestAnimationFrame(() => {
|
|
468
|
+
if (generation !== navigationGeneration) return
|
|
469
|
+
navigationFrame = window.requestAnimationFrame(navigate)
|
|
470
|
+
})
|
|
241
471
|
}
|
|
242
472
|
|
|
243
473
|
function statusSignal(document, state) {
|
|
244
|
-
const signal =
|
|
474
|
+
const signal = state === 'ongoing'
|
|
475
|
+
? document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
476
|
+
: document.createElement('span')
|
|
477
|
+
if (state === 'ongoing') {
|
|
478
|
+
signal.setAttribute('viewBox', '0 0 24 24')
|
|
479
|
+
signal.append(createRunningMarker(document))
|
|
480
|
+
}
|
|
245
481
|
signal.setAttribute('data-prts-session-summary-signal', state)
|
|
246
482
|
signal.setAttribute('aria-hidden', 'true')
|
|
247
483
|
return signal
|
|
@@ -250,6 +486,11 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
250
486
|
function buildSummaryGroup(state, label, items) {
|
|
251
487
|
const group = document.createElement('span')
|
|
252
488
|
group.setAttribute('data-prts-session-summary-group', state)
|
|
489
|
+
group.addEventListener('pointerenter', () => keepSummaryHover(group))
|
|
490
|
+
group.addEventListener('pointerleave', () => {
|
|
491
|
+
window.clearTimeout(summaryLeaveTimer)
|
|
492
|
+
summaryLeaveTimer = window.setTimeout(() => { if (hoveredSummary === group) clearSummaryHover() }, 180)
|
|
493
|
+
})
|
|
253
494
|
|
|
254
495
|
const trigger = document.createElement('button')
|
|
255
496
|
trigger.type = 'button'
|
|
@@ -261,10 +502,20 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
261
502
|
count.setAttribute('data-prts-session-summary-count', '')
|
|
262
503
|
count.textContent = String(items.length)
|
|
263
504
|
trigger.append(count)
|
|
505
|
+
trigger.addEventListener('pointerenter', () => enterPreviewState(state))
|
|
506
|
+
trigger.addEventListener('focus', () => {
|
|
507
|
+
if (trigger.matches(':focus-visible')) {
|
|
508
|
+
summaryDismissed = false
|
|
509
|
+
summaryRoot?.removeAttribute('data-prts-summary-dismissed')
|
|
510
|
+
document.removeEventListener('pointermove', resumeSummaryHover)
|
|
511
|
+
}
|
|
512
|
+
enterPreviewState(state)
|
|
513
|
+
})
|
|
264
514
|
|
|
265
515
|
const popover = document.createElement('span')
|
|
266
516
|
popover.setAttribute('data-prts-session-summary-popover', state)
|
|
267
517
|
popover.setAttribute('role', 'menu')
|
|
518
|
+
popover.setAttribute('popover', 'manual')
|
|
268
519
|
popover.setAttribute('aria-label', label)
|
|
269
520
|
const heading = document.createElement('span')
|
|
270
521
|
heading.setAttribute('data-prts-session-summary-heading', '')
|
|
@@ -287,7 +538,92 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
287
538
|
const list = document.createElement('span')
|
|
288
539
|
list.setAttribute('data-prts-summary-list', '')
|
|
289
540
|
popover.append(list)
|
|
290
|
-
|
|
541
|
+
const spaceNotice = document.createElement('div')
|
|
542
|
+
spaceNotice.setAttribute('data-prts-summary-space-notice', '')
|
|
543
|
+
spaceNotice.textContent = '空间不足,无法显示会话预览。建议扩大窗口,或在辅助功能中关闭预览。'
|
|
544
|
+
popover.append(spaceNotice)
|
|
545
|
+
let boundsFrame
|
|
546
|
+
const checkBounds = () => {
|
|
547
|
+
window.cancelAnimationFrame(boundsFrame)
|
|
548
|
+
boundsFrame = undefined
|
|
549
|
+
if (!group.isConnected) return
|
|
550
|
+
// Escape the host sidebar's clipped region without changing its layout.
|
|
551
|
+
if (typeof popover.showPopover === 'function' && !popover.matches(':popover-open')) popover.showPopover()
|
|
552
|
+
const header = group.closest('[data-prts-session-summary-header]')?.getBoundingClientRect()
|
|
553
|
+
if (!header) return
|
|
554
|
+
Object.assign(popover.style, {position:'fixed', boxSizing:'border-box', margin:'0', left:header.left+'px',
|
|
555
|
+
width:header.width+'px', right:'auto', bottom:'auto'})
|
|
556
|
+
popover.removeAttribute('data-prts-summary-no-space')
|
|
557
|
+
popover.removeAttribute('data-prts-summary-no-room')
|
|
558
|
+
const viewport = window.visualViewport
|
|
559
|
+
const left = viewport?.offsetLeft ?? 0, top = viewport?.offsetTop ?? 0
|
|
560
|
+
const right = left + (viewport?.width ?? window.innerWidth) - 8
|
|
561
|
+
const bottom = top + (viewport?.height ?? window.innerHeight) - 8
|
|
562
|
+
const fits = rect => rect.left >= left + 8 && rect.top >= top + 8 && rect.right <= right && rect.bottom <= bottom
|
|
563
|
+
// Measure the final layout, independent of the opening transform animation.
|
|
564
|
+
const place = () => {
|
|
565
|
+
popover.removeAttribute('data-prts-summary-above')
|
|
566
|
+
const parent = header
|
|
567
|
+
const rect = popover.getBoundingClientRect()
|
|
568
|
+
const height = popover.offsetHeight
|
|
569
|
+
const below = parent.bottom + 6
|
|
570
|
+
const above = parent.top - 6 - height
|
|
571
|
+
const candidate = y => ({left: rect.left, right: rect.right, top: y, bottom: y + height})
|
|
572
|
+
popover.style.top = below + 'px'
|
|
573
|
+
if (fits(candidate(below))) return true
|
|
574
|
+
popover.setAttribute('data-prts-summary-above', '')
|
|
575
|
+
if (fits(candidate(above))) { popover.style.top = above + 'px'; return true }
|
|
576
|
+
popover.removeAttribute('data-prts-summary-above')
|
|
577
|
+
return false
|
|
578
|
+
}
|
|
579
|
+
if (place()) { trigger.removeAttribute('title'); return }
|
|
580
|
+
popover.setAttribute('data-prts-summary-no-space', '')
|
|
581
|
+
trigger.title = spaceNotice.textContent
|
|
582
|
+
if (!place()) popover.setAttribute('data-prts-summary-no-room', '')
|
|
583
|
+
}
|
|
584
|
+
const scheduleBounds = () => {
|
|
585
|
+
if (boundsFrame === undefined) boundsFrame = window.requestAnimationFrame(checkBounds)
|
|
586
|
+
}
|
|
587
|
+
const boundsObserver = window.ResizeObserver ? new window.ResizeObserver(scheduleBounds) : undefined
|
|
588
|
+
boundsObserver?.observe(popover)
|
|
589
|
+
// Re-entering the trigger need not re-enter its group (the list is a descendant).
|
|
590
|
+
// Run after enterPreviewState so measurement sees the current status list.
|
|
591
|
+
trigger.addEventListener('pointerenter', checkBounds)
|
|
592
|
+
trigger.addEventListener('focus', checkBounds)
|
|
593
|
+
window.addEventListener('resize', scheduleBounds)
|
|
594
|
+
document.addEventListener('scroll', scheduleBounds, true)
|
|
595
|
+
window.visualViewport?.addEventListener('resize', scheduleBounds)
|
|
596
|
+
window.visualViewport?.addEventListener('scroll', scheduleBounds)
|
|
597
|
+
const entries = new Map()
|
|
598
|
+
const update = items => {
|
|
599
|
+
scheduleBounds()
|
|
600
|
+
count.textContent = String(items.length)
|
|
601
|
+
trigger.setAttribute('aria-label', `${label} ${items.length}`)
|
|
602
|
+
const ids = new Set(items.map(item => item.id))
|
|
603
|
+
for (const [id, entry] of entries) {
|
|
604
|
+
if (ids.has(id)) continue
|
|
605
|
+
stopTitles.get(entry.button)?.()
|
|
606
|
+
stopTitles.delete(entry.button)
|
|
607
|
+
entry.button.remove()
|
|
608
|
+
entries.delete(id)
|
|
609
|
+
}
|
|
610
|
+
for (const [index, incoming] of items.entries()) {
|
|
611
|
+
const existing = entries.get(incoming.id)
|
|
612
|
+
if (existing) {
|
|
613
|
+
if (existing.item.title !== incoming.title) {
|
|
614
|
+
stopTitles.get(existing.button)?.()
|
|
615
|
+
existing.button.querySelector('[data-prts-summary-title] > span').textContent = incoming.title
|
|
616
|
+
}
|
|
617
|
+
Object.assign(existing.item, incoming)
|
|
618
|
+
let detail = existing.button.querySelector('[data-prts-session-summary-item-status]')
|
|
619
|
+
if (incoming.statusLabel && !detail) { detail = document.createElement('small'); detail.setAttribute('data-prts-session-summary-item-status',''); existing.button.append(detail) }
|
|
620
|
+
if (incoming.statusLabel) detail.textContent = incoming.statusLabel
|
|
621
|
+
else detail?.remove()
|
|
622
|
+
existing.button.setAttribute('aria-label', incoming.statusLabel ? `${incoming.title},${incoming.statusLabel}` : incoming.title)
|
|
623
|
+
if (list.children[index] !== existing.button) list.insertBefore(existing.button, list.children[index] ?? null)
|
|
624
|
+
continue
|
|
625
|
+
}
|
|
626
|
+
const item = {...incoming}
|
|
291
627
|
const button = document.createElement('button')
|
|
292
628
|
button.type = 'button'
|
|
293
629
|
button.setAttribute('role', 'menuitem')
|
|
@@ -299,14 +635,19 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
299
635
|
text.textContent = item.title
|
|
300
636
|
title.append(text)
|
|
301
637
|
button.append(title)
|
|
638
|
+
const viewing = document.createElement('em')
|
|
639
|
+
viewing.setAttribute('data-prts-preview-marker', '')
|
|
640
|
+
viewing.textContent = '预览中'
|
|
641
|
+
button.append(viewing)
|
|
302
642
|
button.setAttribute('aria-label', item.title)
|
|
303
|
-
stopTitles.
|
|
643
|
+
stopTitles.set(button, scrollSummaryTitle(button, title, text, window))
|
|
304
644
|
const showReply = () => {
|
|
645
|
+
if (summaryDismissed) return
|
|
305
646
|
const key = {ongoing:'previewOngoing',warning:'previewWarning',done:'previewDone'}[state]
|
|
306
647
|
if (previewPreferences[key] === false) { preview.stop(); return }
|
|
307
648
|
preview.show(item.id, reply, item.title, button)
|
|
308
649
|
}
|
|
309
|
-
button.addEventListener('
|
|
650
|
+
button.addEventListener('pointermove', showReply)
|
|
310
651
|
button.addEventListener('pointerleave', () => preview.leave())
|
|
311
652
|
button.addEventListener('focus', showReply)
|
|
312
653
|
if (item.statusLabel) {
|
|
@@ -321,8 +662,22 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
321
662
|
preview.stop()
|
|
322
663
|
openSession(item.id)
|
|
323
664
|
})
|
|
324
|
-
list.
|
|
665
|
+
list.insertBefore(button, list.children[index] ?? null)
|
|
666
|
+
entries.set(item.id, {item, button})
|
|
667
|
+
}
|
|
325
668
|
}
|
|
669
|
+
update(items)
|
|
670
|
+
summaryGroups.set(state, {group, update, dispose: () => {
|
|
671
|
+
boundsObserver?.disconnect()
|
|
672
|
+
window.cancelAnimationFrame(boundsFrame)
|
|
673
|
+
window.removeEventListener('resize', scheduleBounds)
|
|
674
|
+
document.removeEventListener('scroll', scheduleBounds, true)
|
|
675
|
+
window.visualViewport?.removeEventListener('resize', scheduleBounds)
|
|
676
|
+
window.visualViewport?.removeEventListener('scroll', scheduleBounds)
|
|
677
|
+
for (const {button} of entries.values()) { stopTitles.get(button)?.(); stopTitles.delete(button) }
|
|
678
|
+
if (hoveredSummary === group) clearSummaryHover()
|
|
679
|
+
group.remove()
|
|
680
|
+
}})
|
|
326
681
|
popover.append(reply)
|
|
327
682
|
group.append(trigger, popover)
|
|
328
683
|
return group
|
|
@@ -331,6 +686,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
331
686
|
function syncSummary(root, next) {
|
|
332
687
|
const { header, label } = resolveSectionHeader(root)
|
|
333
688
|
if (previewPreferences.sessionPreview === false || !header || !label) {
|
|
689
|
+
heldPreviewState = undefined
|
|
334
690
|
preview.stop()
|
|
335
691
|
clearSummaryEffects()
|
|
336
692
|
summaryRoot?.remove()
|
|
@@ -361,10 +717,31 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
361
717
|
rows[index] = { ...rows[index], state: nativeRow.state }
|
|
362
718
|
}
|
|
363
719
|
}
|
|
364
|
-
|
|
365
|
-
const
|
|
366
|
-
|
|
720
|
+
// Do not move the row/menu to another status behind a frozen preview.
|
|
721
|
+
const pinned = preview.pinnedState()
|
|
722
|
+
if (pinned?.state) {
|
|
723
|
+
const item = rows.find(item => item.id === pinned.id)
|
|
724
|
+
if (item) { item.state = pinned.state; item.statusLabel = undefined }
|
|
725
|
+
}
|
|
726
|
+
// While reading a preview, preserve surviving rows' positions and append
|
|
727
|
+
// newcomers. No persistent ordering: the next idle sync uses host order.
|
|
728
|
+
const orderedState = state => {
|
|
729
|
+
const items = rows.filter(item => item.state === state)
|
|
730
|
+
if (!preview.isOpen() || !summaryRoot) return items
|
|
731
|
+
const remaining = new Map(items.map(item => [item.id, item]))
|
|
732
|
+
const stable = []
|
|
733
|
+
for (const row of summaryRoot.querySelectorAll(`[data-prts-session-summary-group="${state}"] [data-prts-session-summary-item]`)) {
|
|
734
|
+
const id = row.getAttribute('data-prts-session-summary-item')
|
|
735
|
+
if (remaining.has(id)) { stable.push(remaining.get(id)); remaining.delete(id) }
|
|
736
|
+
}
|
|
737
|
+
return [...stable, ...remaining.values()]
|
|
738
|
+
}
|
|
739
|
+
const warning = orderedState('warning')
|
|
740
|
+
const ongoing = orderedState('ongoing')
|
|
741
|
+
const done = orderedState('done')
|
|
742
|
+
if (heldPreviewState && !rows.some(item => item.state === heldPreviewState)) heldPreviewState = undefined
|
|
367
743
|
const signature = JSON.stringify({
|
|
744
|
+
heldPreviewState,
|
|
368
745
|
warning: warning.map(item => [item.id, item.title, item.statusLabel]),
|
|
369
746
|
ongoing: ongoing.map(item => [item.id, item.title]),
|
|
370
747
|
done: done.map(item => [item.id, item.title]),
|
|
@@ -381,15 +758,24 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
381
758
|
}
|
|
382
759
|
const labelHidden = Array.from(label.classList).some(name => /hidden/i.test(name))
|
|
383
760
|
summaryRoot.toggleAttribute('data-prts-summary-search-hidden', labelHidden)
|
|
761
|
+
summaryRoot.toggleAttribute('data-prts-summary-dismissed', summaryDismissed)
|
|
384
762
|
summaryRoot.hidden = warning.length === 0 && ongoing.length === 0 && done.length === 0
|
|
385
763
|
if (signature === summarySignature) return
|
|
386
764
|
summarySignature = signature
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
765
|
+
for (const [state, title, items] of [['warning','等待处理的会话',warning],['ongoing','运行中的会话',ongoing],['done','完成未查看',done]]) {
|
|
766
|
+
const existing = summaryGroups.get(state)
|
|
767
|
+
if (!items.length) { existing?.dispose(); summaryGroups.delete(state); continue }
|
|
768
|
+
if (existing) existing.update(items)
|
|
769
|
+
else summaryRoot.append(buildSummaryGroup(state, title, items))
|
|
770
|
+
}
|
|
771
|
+
let position = 0
|
|
772
|
+
for (const state of ['warning','ongoing','done']) {
|
|
773
|
+
const group = summaryGroups.get(state)?.group
|
|
774
|
+
if (group && summaryRoot.children[position++] !== group) summaryRoot.insertBefore(group, summaryRoot.children[position - 1] ?? null)
|
|
775
|
+
group?.removeAttribute('data-prts-preview-open')
|
|
776
|
+
}
|
|
392
777
|
preview.reanchor(summaryRoot)
|
|
778
|
+
if (heldPreviewState) summaryRoot.querySelector(`[data-prts-session-summary-group="${heldPreviewState}"]`)?.setAttribute('data-prts-preview-open', '')
|
|
393
779
|
}
|
|
394
780
|
|
|
395
781
|
function scheduleVectors() {
|
|
@@ -451,7 +837,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
451
837
|
|
|
452
838
|
|
|
453
839
|
function queueScan() {
|
|
454
|
-
if (scanFrame !== undefined) return
|
|
840
|
+
if (!adapterActive || scanFrame !== undefined) return
|
|
455
841
|
const requestFrame = window?.requestAnimationFrame?.bind(window) ?? (callback => window?.setTimeout?.(callback, 0))
|
|
456
842
|
scanFrame = requestFrame(() => {
|
|
457
843
|
scanFrame = undefined
|
|
@@ -460,7 +846,8 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
460
846
|
}
|
|
461
847
|
|
|
462
848
|
function scan() {
|
|
463
|
-
|
|
849
|
+
if (!adapterActive) return
|
|
850
|
+
syncSummarySources()
|
|
464
851
|
const root = document?.querySelector?.('[data-prts-region="sessions"] [data-slot="sidebar.workspaces"]')
|
|
465
852
|
const next = new Set()
|
|
466
853
|
const nextVectorOwners = new Set()
|
|
@@ -489,16 +876,31 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
489
876
|
registerVector(controls.menu, { kind: 'button' }, next, nextVectorOwners)
|
|
490
877
|
registerVector(controls.create, { kind: 'button' }, next, nextVectorOwners)
|
|
491
878
|
}
|
|
879
|
+
const rowSessionId = row => row.getAttribute('data-session-id') || row.getAttribute('data-row-key')?.replace(/^session:/, '')
|
|
880
|
+
if (selectionTarget && Array.from(root.querySelectorAll('[role="treeitem"][aria-selected="true"]')).some(row => rowSessionId(row) === selectionTarget)) clearSelectionTarget()
|
|
492
881
|
let sessionOrdinal = 0
|
|
493
882
|
for (const row of root.querySelectorAll('div[role="treeitem"][aria-selected]')) {
|
|
494
883
|
const sessionIndex = String(++sessionOrdinal).padStart(2, '0')
|
|
495
884
|
const controls = resolveRowControls(row, 'session')
|
|
496
885
|
const state = nativeSessionState(row)
|
|
886
|
+
const spinner = state === 'ongoing' ? row.querySelector('svg[data-state="ongoing"]') : undefined
|
|
887
|
+
if (spinner) {
|
|
888
|
+
let marker = spinner.querySelector('[data-prts-running-marker]')
|
|
889
|
+
if (!marker) {
|
|
890
|
+
marker = createRunningMarker(document)
|
|
891
|
+
spinner.append(marker)
|
|
892
|
+
}
|
|
893
|
+
next.add(marker)
|
|
894
|
+
}
|
|
895
|
+
|
|
497
896
|
const face = ensureFacilityFace(document, row, 'session', next)
|
|
498
897
|
const spine = ensureFacilitySpine(document, row, 'session', next)
|
|
499
898
|
registerVector(face, { kind: 'face', topNotch: true, sessionNotch: true, texture: 'pickup' }, next, nextVectorOwners)
|
|
500
899
|
registerVector(spine, { kind: 'spine' }, next, nextVectorOwners)
|
|
501
|
-
|
|
900
|
+
const selected = selectionTarget ? rowSessionId(row) === selectionTarget : row.getAttribute('aria-selected') === 'true'
|
|
901
|
+
if (selectionTarget) row.setAttribute('data-prts-selection-target', String(selected))
|
|
902
|
+
else row.removeAttribute('data-prts-selection-target')
|
|
903
|
+
if (!activeLifelineRow && selected) {
|
|
502
904
|
activeLifelineRow = row
|
|
503
905
|
activeLifeline = face.querySelector('[data-prts-session-lifeline]')
|
|
504
906
|
}
|
|
@@ -533,7 +935,8 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
533
935
|
lifelineAnimator.setTarget(activeLifelineRow, activeLifeline)
|
|
534
936
|
for (const node of owned) {
|
|
535
937
|
if (next.has(node)) continue
|
|
536
|
-
if (node.hasAttribute?.('data-prts-
|
|
938
|
+
if (node.hasAttribute?.('data-prts-running-marker')
|
|
939
|
+
|| node.hasAttribute?.('data-prts-owned-projection')
|
|
537
940
|
|| node.hasAttribute?.('data-prts-owned-facility')
|
|
538
941
|
|| node.hasAttribute?.('data-prts-owned-facility-spine')
|
|
539
942
|
|| node.hasAttribute?.('data-prts-owned-facility-vector')) {
|
|
@@ -544,13 +947,14 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
544
947
|
}
|
|
545
948
|
syncVectorOwners(nextVectorOwners)
|
|
546
949
|
owned = next
|
|
950
|
+
syncSessionReveal(root)
|
|
547
951
|
}
|
|
548
952
|
|
|
549
953
|
function clear() {
|
|
550
954
|
preview.stop()
|
|
551
955
|
clearSummaryEffects()
|
|
552
956
|
lifelineAnimator.setTarget(undefined, undefined)
|
|
553
|
-
for (const node of document?.querySelectorAll?.('[data-prts-owned-projection], [data-prts-owned-facility], [data-prts-owned-facility-spine], [data-prts-owned-facility-vector], [data-prts-owned-facility-defs]') ?? []) node.remove()
|
|
957
|
+
for (const node of document?.querySelectorAll?.('[data-prts-running-marker], [data-prts-owned-projection], [data-prts-owned-facility], [data-prts-owned-facility-spine], [data-prts-owned-facility-vector], [data-prts-owned-facility-defs]') ?? []) node.remove()
|
|
554
958
|
for (const node of owned) {
|
|
555
959
|
for (const attribute of OWNED_ATTRIBUTES) node.removeAttribute?.(attribute)
|
|
556
960
|
}
|
|
@@ -567,10 +971,11 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
567
971
|
|
|
568
972
|
return {
|
|
569
973
|
start(preferences = {}) {
|
|
570
|
-
const keys = ['sessionPreview','previewOngoing','previewWarning','previewDone','previewDoubleClick']
|
|
974
|
+
const keys = ['sessionPreview','previewOngoing','previewWarning','previewDone','previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning', 'previewReplyLimit']
|
|
571
975
|
if (keys.some(key => previewPreferences[key] !== preferences[key])) preview.stop()
|
|
572
976
|
previewPreferences = Object.fromEntries(keys.map(key => [key, preferences[key]]))
|
|
573
977
|
if (!document) return
|
|
978
|
+
adapterActive = true
|
|
574
979
|
lifelineAnimator.start()
|
|
575
980
|
ensureFacilityTextureDefs(document)
|
|
576
981
|
const ResizeObserver = window?.ResizeObserver
|
|
@@ -586,16 +991,22 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
586
991
|
})
|
|
587
992
|
}
|
|
588
993
|
scan()
|
|
589
|
-
if (!statusSourcesStarted) {
|
|
590
|
-
statusSourcesStarted = true
|
|
591
|
-
stopSessionList = sessions?.list?.subscribe?.(queueScan) ?? (() => {})
|
|
592
|
-
stopWorkspaceList = workspaces?.list?.subscribe?.(queueScan) ?? (() => {})
|
|
593
|
-
}
|
|
594
994
|
if (observer || !window?.MutationObserver) return
|
|
595
995
|
const observationRoot = document.querySelector('[data-prts-region="sessions"]')
|
|
596
996
|
if (!observationRoot) return
|
|
597
997
|
observer = new window.MutationObserver(mutations => {
|
|
598
|
-
|
|
998
|
+
const relevant = mutations.filter(mutation => !mutation.target.closest?.('[data-prts-owned-session-summary]'))
|
|
999
|
+
const freshRow = 'div[role="treeitem"]:not([data-prts-session-row]):not([data-prts-workspace-row])'
|
|
1000
|
+
const mountsRows = relevant.some(mutation => mutation.type === 'childList'
|
|
1001
|
+
&& Array.from(mutation.addedNodes).some(node => node.nodeType === 1
|
|
1002
|
+
&& (node.matches(freshRow) || node.querySelector(freshRow))))
|
|
1003
|
+
if (mountsRows) {
|
|
1004
|
+
// React has mounted expanded rows. Finish theme geometry before their first paint.
|
|
1005
|
+
window.cancelAnimationFrame?.(scanFrame)
|
|
1006
|
+
scanFrame = undefined
|
|
1007
|
+
scan()
|
|
1008
|
+
flushVectors()
|
|
1009
|
+
} else if (relevant.length) queueScan()
|
|
599
1010
|
})
|
|
600
1011
|
observer.observe(observationRoot, {
|
|
601
1012
|
subtree: true,
|
|
@@ -605,16 +1016,16 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
605
1016
|
})
|
|
606
1017
|
},
|
|
607
1018
|
dispose() {
|
|
1019
|
+
adapterActive = false
|
|
1020
|
+
disconnectSummarySources()
|
|
1021
|
+
cancelReveal()
|
|
1022
|
+
document.removeEventListener('pointermove', resumeSummaryHover)
|
|
1023
|
+
summaryDismissed = false
|
|
1024
|
+
heldPreviewState = undefined
|
|
1025
|
+
document?.removeEventListener('pointerdown', dismissHeldPreview)
|
|
1026
|
+
document?.removeEventListener('keydown', dismissHeldPreview)
|
|
608
1027
|
observer?.disconnect()
|
|
609
1028
|
observer = undefined
|
|
610
|
-
stopSessionList()
|
|
611
|
-
stopSessionList = () => {}
|
|
612
|
-
stopWorkspaceList()
|
|
613
|
-
stopWorkspaceList = () => {}
|
|
614
|
-
stopSessionStatus()
|
|
615
|
-
stopSessionStatus = () => {}
|
|
616
|
-
sessionStatus = undefined
|
|
617
|
-
statusSourcesStarted = false
|
|
618
1029
|
if (scanFrame !== undefined) {
|
|
619
1030
|
if (window?.cancelAnimationFrame) window.cancelAnimationFrame(scanFrame)
|
|
620
1031
|
else window?.clearTimeout?.(scanFrame)
|