@ash-qw/dsh-theme-prts 1.4.1 → 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.
@@ -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
- export function createSidebarControlAdapter({ document, window, sessions, workspaces, getSessionStatus, getUiWorkspace, getUiConversation, getSessions }) {
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
@@ -204,6 +220,46 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
204
220
  let summaryRoot
205
221
  let summarySignature = ''
206
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
+ }
207
263
  function dismissHeldPreview(event) {
208
264
  if (!heldPreviewState || (event?.type === 'keydown' && event.key !== 'Escape')) return
209
265
  if (event?.type === 'pointerdown' && (summaryRoot?.contains(event.target) || event.target.closest?.('[data-prts-summary-reply]'))) return
@@ -217,13 +273,70 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
217
273
  const preview = createSessionSummaryPreview({ document, window, sessions, getUiConversation, getSessions, getSessionStatus, openSession, getPreviewPreferences: () => previewPreferences, onPreviewClosed: () => { if (statusSourcesStarted) queueScan() }, onAnswerSubmitted: state => { heldPreviewState = state; queueScan() } })
218
274
  const stopTitles = new Map()
219
275
  const summaryGroups = new Map()
220
- function clearSummaryEffects() { preview.stop(false); for (const stop of stopTitles.values()) stop(); stopTitles.clear(); summaryGroups.clear() }
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() }
221
277
  let owned = new Set()
222
278
  let vectorOwners = new Set()
223
279
  const dirtyVectors = new Map()
224
280
  const vectorSizes = new WeakMap()
225
281
  const lifelineAnimator = createSessionLifelineAnimator({ document, window })
226
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
+ }
227
340
  function connectSessionStatus() {
228
341
  const source = getSessionStatus?.()
229
342
  if (source === sessionStatus) return
@@ -232,29 +345,139 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
232
345
  stopSessionStatus = source?.subscribe?.(queueScan) ?? (() => {})
233
346
  }
234
347
 
235
- function openSession(sessionId) {
236
- dismissHeldPreview()
237
- try {
238
- const uiWorkspace = getUiWorkspace?.()
239
- if (typeof uiWorkspace?.openSession === 'function') {
240
- uiWorkspace.openSession(sessionId)
241
- return
242
- }
243
- if (typeof sessions?.open === 'function') {
244
- sessions.open(sessionId)
245
- return
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 }
246
432
  }
247
- } catch {}
248
- const row = Array.from(document.querySelectorAll('[data-prts-session-row]')).find(candidate => (
249
- candidate.getAttribute('data-row-key') === `session:${sessionId}`
250
- || candidate.getAttribute('data-session-id') === sessionId
251
- || candidate.querySelector('[data-prts-row-title]')?.textContent.trim() === sessionId
252
- ))
253
- row?.click?.()
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
+ })
254
471
  }
255
472
 
256
473
  function statusSignal(document, state) {
257
- const signal = document.createElement('span')
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
+ }
258
481
  signal.setAttribute('data-prts-session-summary-signal', state)
259
482
  signal.setAttribute('aria-hidden', 'true')
260
483
  return signal
@@ -263,6 +486,11 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
263
486
  function buildSummaryGroup(state, label, items) {
264
487
  const group = document.createElement('span')
265
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
+ })
266
494
 
267
495
  const trigger = document.createElement('button')
268
496
  trigger.type = 'button'
@@ -275,11 +503,19 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
275
503
  count.textContent = String(items.length)
276
504
  trigger.append(count)
277
505
  trigger.addEventListener('pointerenter', () => enterPreviewState(state))
278
- trigger.addEventListener('focus', () => 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
+ })
279
514
 
280
515
  const popover = document.createElement('span')
281
516
  popover.setAttribute('data-prts-session-summary-popover', state)
282
517
  popover.setAttribute('role', 'menu')
518
+ popover.setAttribute('popover', 'manual')
283
519
  popover.setAttribute('aria-label', label)
284
520
  const heading = document.createElement('span')
285
521
  heading.setAttribute('data-prts-session-summary-heading', '')
@@ -302,8 +538,65 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
302
538
  const list = document.createElement('span')
303
539
  list.setAttribute('data-prts-summary-list', '')
304
540
  popover.append(list)
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)
305
597
  const entries = new Map()
306
598
  const update = items => {
599
+ scheduleBounds()
307
600
  count.textContent = String(items.length)
308
601
  trigger.setAttribute('aria-label', `${label} ${items.length}`)
309
602
  const ids = new Set(items.map(item => item.id))
@@ -349,6 +642,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
349
642
  button.setAttribute('aria-label', item.title)
350
643
  stopTitles.set(button, scrollSummaryTitle(button, title, text, window))
351
644
  const showReply = () => {
645
+ if (summaryDismissed) return
352
646
  const key = {ongoing:'previewOngoing',warning:'previewWarning',done:'previewDone'}[state]
353
647
  if (previewPreferences[key] === false) { preview.stop(); return }
354
648
  preview.show(item.id, reply, item.title, button)
@@ -374,7 +668,14 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
374
668
  }
375
669
  update(items)
376
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)
377
677
  for (const {button} of entries.values()) { stopTitles.get(button)?.(); stopTitles.delete(button) }
678
+ if (hoveredSummary === group) clearSummaryHover()
378
679
  group.remove()
379
680
  }})
380
681
  popover.append(reply)
@@ -457,6 +758,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
457
758
  }
458
759
  const labelHidden = Array.from(label.classList).some(name => /hidden/i.test(name))
459
760
  summaryRoot.toggleAttribute('data-prts-summary-search-hidden', labelHidden)
761
+ summaryRoot.toggleAttribute('data-prts-summary-dismissed', summaryDismissed)
460
762
  summaryRoot.hidden = warning.length === 0 && ongoing.length === 0 && done.length === 0
461
763
  if (signature === summarySignature) return
462
764
  summarySignature = signature
@@ -535,7 +837,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
535
837
 
536
838
 
537
839
  function queueScan() {
538
- if (scanFrame !== undefined) return
840
+ if (!adapterActive || scanFrame !== undefined) return
539
841
  const requestFrame = window?.requestAnimationFrame?.bind(window) ?? (callback => window?.setTimeout?.(callback, 0))
540
842
  scanFrame = requestFrame(() => {
541
843
  scanFrame = undefined
@@ -544,7 +846,8 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
544
846
  }
545
847
 
546
848
  function scan() {
547
- connectSessionStatus()
849
+ if (!adapterActive) return
850
+ syncSummarySources()
548
851
  const root = document?.querySelector?.('[data-prts-region="sessions"] [data-slot="sidebar.workspaces"]')
549
852
  const next = new Set()
550
853
  const nextVectorOwners = new Set()
@@ -573,16 +876,31 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
573
876
  registerVector(controls.menu, { kind: 'button' }, next, nextVectorOwners)
574
877
  registerVector(controls.create, { kind: 'button' }, next, nextVectorOwners)
575
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()
576
881
  let sessionOrdinal = 0
577
882
  for (const row of root.querySelectorAll('div[role="treeitem"][aria-selected]')) {
578
883
  const sessionIndex = String(++sessionOrdinal).padStart(2, '0')
579
884
  const controls = resolveRowControls(row, 'session')
580
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
+
581
896
  const face = ensureFacilityFace(document, row, 'session', next)
582
897
  const spine = ensureFacilitySpine(document, row, 'session', next)
583
898
  registerVector(face, { kind: 'face', topNotch: true, sessionNotch: true, texture: 'pickup' }, next, nextVectorOwners)
584
899
  registerVector(spine, { kind: 'spine' }, next, nextVectorOwners)
585
- if (!activeLifelineRow && row.getAttribute('aria-selected') === 'true') {
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) {
586
904
  activeLifelineRow = row
587
905
  activeLifeline = face.querySelector('[data-prts-session-lifeline]')
588
906
  }
@@ -617,7 +935,8 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
617
935
  lifelineAnimator.setTarget(activeLifelineRow, activeLifeline)
618
936
  for (const node of owned) {
619
937
  if (next.has(node)) continue
620
- if (node.hasAttribute?.('data-prts-owned-projection')
938
+ if (node.hasAttribute?.('data-prts-running-marker')
939
+ || node.hasAttribute?.('data-prts-owned-projection')
621
940
  || node.hasAttribute?.('data-prts-owned-facility')
622
941
  || node.hasAttribute?.('data-prts-owned-facility-spine')
623
942
  || node.hasAttribute?.('data-prts-owned-facility-vector')) {
@@ -628,13 +947,14 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
628
947
  }
629
948
  syncVectorOwners(nextVectorOwners)
630
949
  owned = next
950
+ syncSessionReveal(root)
631
951
  }
632
952
 
633
953
  function clear() {
634
954
  preview.stop()
635
955
  clearSummaryEffects()
636
956
  lifelineAnimator.setTarget(undefined, undefined)
637
- 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()
638
958
  for (const node of owned) {
639
959
  for (const attribute of OWNED_ATTRIBUTES) node.removeAttribute?.(attribute)
640
960
  }
@@ -655,6 +975,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
655
975
  if (keys.some(key => previewPreferences[key] !== preferences[key])) preview.stop()
656
976
  previewPreferences = Object.fromEntries(keys.map(key => [key, preferences[key]]))
657
977
  if (!document) return
978
+ adapterActive = true
658
979
  lifelineAnimator.start()
659
980
  ensureFacilityTextureDefs(document)
660
981
  const ResizeObserver = window?.ResizeObserver
@@ -669,19 +990,23 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
669
990
  flushVectors()
670
991
  })
671
992
  }
672
- document.addEventListener('pointerdown', dismissHeldPreview)
673
- document.addEventListener('keydown', dismissHeldPreview)
674
993
  scan()
675
- if (!statusSourcesStarted) {
676
- statusSourcesStarted = true
677
- stopSessionList = sessions?.list?.subscribe?.(queueScan) ?? (() => {})
678
- stopWorkspaceList = workspaces?.list?.subscribe?.(queueScan) ?? (() => {})
679
- }
680
994
  if (observer || !window?.MutationObserver) return
681
995
  const observationRoot = document.querySelector('[data-prts-region="sessions"]')
682
996
  if (!observationRoot) return
683
997
  observer = new window.MutationObserver(mutations => {
684
- if (mutations.some(mutation => !mutation.target.closest?.('[data-prts-owned-session-summary]'))) queueScan()
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()
685
1010
  })
686
1011
  observer.observe(observationRoot, {
687
1012
  subtree: true,
@@ -691,19 +1016,16 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
691
1016
  })
692
1017
  },
693
1018
  dispose() {
1019
+ adapterActive = false
1020
+ disconnectSummarySources()
1021
+ cancelReveal()
1022
+ document.removeEventListener('pointermove', resumeSummaryHover)
1023
+ summaryDismissed = false
694
1024
  heldPreviewState = undefined
695
1025
  document?.removeEventListener('pointerdown', dismissHeldPreview)
696
1026
  document?.removeEventListener('keydown', dismissHeldPreview)
697
1027
  observer?.disconnect()
698
1028
  observer = undefined
699
- stopSessionList()
700
- stopSessionList = () => {}
701
- stopWorkspaceList()
702
- stopWorkspaceList = () => {}
703
- stopSessionStatus()
704
- stopSessionStatus = () => {}
705
- sessionStatus = undefined
706
- statusSourcesStarted = false
707
1029
  if (scanFrame !== undefined) {
708
1030
  if (window?.cancelAnimationFrame) window.cancelAnimationFrame(scanFrame)
709
1031
  else window?.clearTimeout?.(scanFrame)