@ash-qw/dsh-theme-prts 2.1.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.en.md +8 -0
- package/README.md +8 -0
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/THIRD_PARTY_NOTICES.zh-CN.md +1 -1
- package/lib/client.js +556 -46
- package/package.json +2 -2
- package/src/client/conversation-control-adapter.js +2 -0
- package/src/client/custom-background.js +2 -1
- package/src/client/index.js +8 -0
- package/src/client/native-preferences.js +31 -0
- package/src/client/operations-shell-v2.js +2 -2
- package/src/client/particle-field-adapter.js +67 -14
- package/src/client/preferences.js +9 -2
- package/src/client/session-summary-preview.js +148 -16
- package/src/client/sidebar-control-adapter.js +1 -1
- package/src/client/theme-controller-v2.js +2 -1
- package/src/client/theme-settings-workbench.js +42 -5
- package/src/client/theme-transfer-panel.js +98 -0
- package/src/client/theme-transfer.js +167 -0
- package/src/index.js +21 -4
- package/src/styles/custom-background.css +14 -0
- package/src/styles/facility-vector.css +26 -4
- package/src/styles/refinement.css +22 -0
- package/src/styles/theme-settings-workbench.css +97 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ash-qw/dsh-theme-prts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Unofficial Arknights P.R.T.S.-inspired UI theme for DeepSeek Harness Web and the official desktop client",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"@playwright/test": "1.55.1",
|
|
76
76
|
"esbuild": "^0.28.2",
|
|
77
77
|
"jsdom": "24.1.3",
|
|
78
|
-
"markdown-it": "^14.3.
|
|
78
|
+
"markdown-it": "^14.3.3"
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@deepseek-ai/schemastery": "~3.18.4",
|
|
@@ -14,6 +14,8 @@ function closestWithClassSuffix(node, suffix) {
|
|
|
14
14
|
function classify(button, operation) {
|
|
15
15
|
// The host's process disclosure owns its transparent, full-width hit area.
|
|
16
16
|
if (button.hasAttribute('data-turn-process')) return null
|
|
17
|
+
// Notification headers belong to the card surface, not standalone buttons.
|
|
18
|
+
if (button.parentElement?.matches('[data-turn-trigger]')) return null
|
|
17
19
|
// Changed-file cards own one outer surface; their header and rows stay native.
|
|
18
20
|
if (button.closest('[data-changed-files]')) return null
|
|
19
21
|
// File previews are transparent hit targets; the enclosing card owns its material and outline.
|
|
@@ -63,7 +63,7 @@ export function createBackgroundStore({ window, document }) {
|
|
|
63
63
|
},
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
export function backgroundMarkup() {
|
|
66
|
+
export function backgroundMarkup(heroContrastControl = '') {
|
|
67
67
|
return `<section data-prts-background-settings>
|
|
68
68
|
<header><strong>会话背景</strong><small>分区使用不同图片,或让一张完整背景贯穿侧边栏与正文。</small></header>
|
|
69
69
|
<div data-prts-background-modes role="group" aria-label="背景模式">
|
|
@@ -75,6 +75,7 @@ export function backgroundMarkup() {
|
|
|
75
75
|
<input type="file" data-prts-background-file="${slot}" accept="image/png,image/jpeg,image/webp,image/avif" hidden>
|
|
76
76
|
</article>`).join('')}</div>
|
|
77
77
|
<label data-prts-background-shade hidden><span>背景淡化 <output></output></span><input type="range" min="0" max="90" step="5" aria-label="背景淡化程度"><small>调高后背景更淡,接近当前主题底色。</small></label>
|
|
78
|
+
<div data-prts-background-hero-setting hidden>${heroContrastControl}</div>
|
|
78
79
|
${opacityDialMarkup()}
|
|
79
80
|
<small data-prts-background-help>支持 PNG / JPEG / WebP / AVIF · 最大 10 MB<br>图片居中铺满,切换模式保留素材,并在当前 DSH 实例同步。</small><p data-prts-background-status role="status"></p>
|
|
80
81
|
</section>`
|
package/src/client/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createThemeTransfer } from './theme-transfer.js'
|
|
1
2
|
import { createBackgroundStore } from './custom-background.js'
|
|
2
3
|
import { createNativePreferences } from './native-preferences.js'
|
|
3
4
|
import { createComposerGlassSurface } from './composer-glass-surface.js'
|
|
@@ -91,6 +92,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
91
92
|
cssText,
|
|
92
93
|
service: themeService,
|
|
93
94
|
cursorAssets: assets.cursor,
|
|
95
|
+
onSchemeChange() { particleField.repaintScheme() },
|
|
94
96
|
onTransitionStateChange(active) {
|
|
95
97
|
particleField.setSchemeTransitionActive(active)
|
|
96
98
|
},
|
|
@@ -111,6 +113,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
111
113
|
const adapter = createRc7Adapter({ document })
|
|
112
114
|
const hostGeometry = createHostGeometryAdapter({ document, window })
|
|
113
115
|
const connection = contextService(ctx, 'connection')
|
|
116
|
+
let themeTransfer
|
|
114
117
|
let operations
|
|
115
118
|
let mountObserver
|
|
116
119
|
let mountFrame
|
|
@@ -201,6 +204,9 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
201
204
|
if (!themeAlreadyApplied) theme.apply(preferences)
|
|
202
205
|
operations ||= createOperationsShell({
|
|
203
206
|
document, window, assets, adapter, hostGeometry, customEmblemStore, avatarStore, backgroundStore,
|
|
207
|
+
themeTransfer: themeTransfer ||= createThemeTransfer({ window, getPreferences: () => nativePreferences.value,
|
|
208
|
+
savePreferences: (value, revision) => nativePreferences.replace(value, revision),
|
|
209
|
+
checkWritable: () => nativePreferences.checkWritable(), checkExport: () => nativePreferences.checkReadable(), themeVersion: version }),
|
|
204
210
|
onSchemeToggle(next, interaction) {
|
|
205
211
|
return theme.setTheme(next, { ...interaction, animate: true })
|
|
206
212
|
},
|
|
@@ -347,6 +353,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
347
353
|
} else {
|
|
348
354
|
applyVisualState()
|
|
349
355
|
}
|
|
356
|
+
const stopBackgroundRedraw = backgroundStore.subscribe(() => { if (!safeMode) particleField.update(preferences) })
|
|
350
357
|
const bindNativePreferences = settingsCtx => {
|
|
351
358
|
const forms = contextService(settingsCtx, 'configForms')
|
|
352
359
|
if (forms) settingsCtx.effect(() => nativePreferences.bind(forms.get('theme-prts')))
|
|
@@ -359,6 +366,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
359
366
|
statusSource.dispose()
|
|
360
367
|
customEmblemStore.dispose()
|
|
361
368
|
avatarStore.dispose()
|
|
369
|
+
stopBackgroundRedraw()
|
|
362
370
|
backgroundStore.dispose()
|
|
363
371
|
particleField.dispose()
|
|
364
372
|
startup.dispose({ restoreFocus: false })
|
|
@@ -69,6 +69,37 @@ export function createNativePreferences({ storage, onChange }) {
|
|
|
69
69
|
}
|
|
70
70
|
return {
|
|
71
71
|
get value() { return current },
|
|
72
|
+
checkReadable() {
|
|
73
|
+
const s = snapshot()
|
|
74
|
+
if (disposed || s?.status !== 'ready') throw Error('请等待 DSH 设置加载完成')
|
|
75
|
+
if (busy || Object.keys(pending).length || Object.keys(failed).length) throw Error('请先等待当前设置保存成功')
|
|
76
|
+
return s.revision
|
|
77
|
+
},
|
|
78
|
+
checkWritable() {
|
|
79
|
+
const revision = this.checkReadable()
|
|
80
|
+
if (!writable(snapshot())) throw Error('当前连接无法保存主题设置')
|
|
81
|
+
return revision
|
|
82
|
+
},
|
|
83
|
+
async replace(next, expectedRevision) {
|
|
84
|
+
this.checkWritable()
|
|
85
|
+
const s = snapshot()
|
|
86
|
+
if (s.revision !== expectedRevision) throw Error('设置已在其他页面更新,请重新导入')
|
|
87
|
+
const normalized = normalizePreferences(next)
|
|
88
|
+
busy = true
|
|
89
|
+
phase = 'saving'; emit()
|
|
90
|
+
try {
|
|
91
|
+
const ops = Object.entries(normalized).map(([key, value]) => ({ op: 'set', path: [key], value }))
|
|
92
|
+
ops.push({ op: 'set', path: ['preferencesInitialized'], value: true })
|
|
93
|
+
if (await form.mutate(ops, s.revision) !== true) throw Error('DSH 未接受导入设置,请重试')
|
|
94
|
+
phase = 'saved'
|
|
95
|
+
} catch (error) {
|
|
96
|
+
phase = 'error'
|
|
97
|
+
throw error
|
|
98
|
+
} finally {
|
|
99
|
+
busy = false
|
|
100
|
+
receive()
|
|
101
|
+
}
|
|
102
|
+
},
|
|
72
103
|
bind(next) {
|
|
73
104
|
unsubscribe?.(); form = next
|
|
74
105
|
unsubscribe = form.subscribe(receive)
|
|
@@ -46,7 +46,7 @@ export function createOperationsShell({
|
|
|
46
46
|
onSchemeToggle = () => {},
|
|
47
47
|
onSchemeDragStart = () => undefined,
|
|
48
48
|
onThemeDisable = () => {},
|
|
49
|
-
customEmblemStore, avatarStore, backgroundStore,
|
|
49
|
+
customEmblemStore, avatarStore, backgroundStore, themeTransfer,
|
|
50
50
|
onPreferenceChange = () => {},
|
|
51
51
|
onPreferencePreview = () => {},
|
|
52
52
|
onVisualReset = () => {},
|
|
@@ -481,7 +481,7 @@ export function createOperationsShell({
|
|
|
481
481
|
trigger: shell.querySelector('[data-prts-rail-brand]'),
|
|
482
482
|
panel: shell.querySelector('[data-prts-theme-settings]'),
|
|
483
483
|
backdrop: shell.querySelector('[data-prts-settings-backdrop]'),
|
|
484
|
-
customEmblemStore, avatarStore, backgroundStore,
|
|
484
|
+
customEmblemStore, avatarStore, backgroundStore, themeTransfer,
|
|
485
485
|
onPreferenceChange,
|
|
486
486
|
onPreferencePreview,
|
|
487
487
|
onResetVisual: onVisualReset,
|
|
@@ -432,7 +432,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
432
432
|
const densityProfile = density => PARTICLE_DENSITY_PROFILES[density] ?? PARTICLE_DENSITY_PROFILES.standard
|
|
433
433
|
const particleRadius = density => densityProfile(density ?? activeDensity()).radius
|
|
434
434
|
const fieldEnabled = () => heroActive || (preferences.conversationStyle !== 'deck-chat' && preferences.conversationEmblem !== false)
|
|
435
|
-
const canRenderFrame = () => fieldEnabled() && !disposed && documentVisible && canvasVisible && !hydrationPaused && !startupPaused && !schemeTransitionPaused && !resizing
|
|
435
|
+
const canRenderFrame = (paintOnly = false) => fieldEnabled() && !disposed && documentVisible && canvasVisible && !hydrationPaused && !startupPaused && (paintOnly || !schemeTransitionPaused) && !resizing
|
|
436
436
|
const layoutWidth = () => width
|
|
437
437
|
const layoutHeight = () => height
|
|
438
438
|
const layoutShiftX = () => 0
|
|
@@ -1134,12 +1134,25 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1134
1134
|
resizeTimer = window.setTimeout?.(finishResizeTransaction, RESIZE_SETTLE_MS)
|
|
1135
1135
|
}
|
|
1136
1136
|
|
|
1137
|
+
function resizeHeroTransition(nextSize) {
|
|
1138
|
+
// Keep live positions and velocities; the spring follows the new destination.
|
|
1139
|
+
// Snapping to particle.target here would restore the old hero mid-explosion.
|
|
1140
|
+
clearResizeState()
|
|
1141
|
+
pointerActive = false
|
|
1142
|
+
resizeCommits += applyFieldSize(nextSize) ? 1 : 0
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1137
1145
|
function onOperationResize(entries = []) {
|
|
1138
1146
|
if (!operation || !canvas) return
|
|
1139
1147
|
resizeObservations += 1
|
|
1140
1148
|
const entry = entries.find(candidate => candidate.target === operation) ?? entries[0]
|
|
1141
1149
|
const contentRect = entry?.contentRect
|
|
1142
1150
|
pendingResizeSize = contentRect ? sizeFromRect(contentRect) : readOperationSize()
|
|
1151
|
+
if (transition?.fromHero && !phoneStatic()) {
|
|
1152
|
+
resizeHeroTransition(pendingResizeSize)
|
|
1153
|
+
updateFrameGate()
|
|
1154
|
+
return
|
|
1155
|
+
}
|
|
1143
1156
|
if (phoneStatic()) {
|
|
1144
1157
|
const nextSize = pendingResizeSize
|
|
1145
1158
|
clearResizeState()
|
|
@@ -1289,21 +1302,30 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1289
1302
|
if (queuedDensity !== undefined && (queuedDensity !== particles.length || force)) startDensityReassembly(queuedDensity, force)
|
|
1290
1303
|
}
|
|
1291
1304
|
|
|
1292
|
-
function
|
|
1305
|
+
function heroContrastStrength() {
|
|
1306
|
+
const root = document.documentElement
|
|
1307
|
+
const imageVisible = preferences.backgroundMode === 'full'
|
|
1308
|
+
? root.hasAttribute('data-prts-background-full')
|
|
1309
|
+
: preferences.backgroundMode === 'split' && root.hasAttribute('data-prts-background-conversation')
|
|
1310
|
+
if (preferences.heroContrast === false || !imageVisible) return 0
|
|
1311
|
+
return heroActive ? 1 : transition?.fromHero ? 1 - easeInOut(transition.progress) : 0
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function draw(timestamp, paintOnly = false) {
|
|
1293
1315
|
frame = undefined
|
|
1294
1316
|
if (!ambient || !canvas) return
|
|
1295
|
-
if (!canRenderFrame()) return
|
|
1317
|
+
if (!canRenderFrame(paintOnly)) return
|
|
1296
1318
|
if (geometryDirty) {
|
|
1297
1319
|
syncCanvasBackingStore()
|
|
1298
1320
|
geometryDirty = false
|
|
1299
1321
|
}
|
|
1300
|
-
if (transition) {
|
|
1322
|
+
if (transition && !paintOnly) {
|
|
1301
1323
|
transitionStartedAt ??= timestamp
|
|
1302
1324
|
transition.progress = clamp((timestamp - transitionStartedAt) / TRANSITION_MS, 0, 1)
|
|
1303
1325
|
if (transition.progress >= 1) completeTransition()
|
|
1304
1326
|
}
|
|
1305
1327
|
|
|
1306
|
-
advanceDensity(timestamp)
|
|
1328
|
+
if (!paintOnly) advanceDensity(timestamp)
|
|
1307
1329
|
const reducedMotion = motionReduced()
|
|
1308
1330
|
const staticPhone = phoneStatic()
|
|
1309
1331
|
const staticField = reducedMotion || staticPhone
|
|
@@ -1322,7 +1344,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1322
1344
|
}
|
|
1323
1345
|
const frameGeometry = { activeLayout, staticField, transitionLayout }
|
|
1324
1346
|
const { center, size: fieldSize } = activeLayout
|
|
1325
|
-
const follow = reducedMotion ? 1 : 0.035
|
|
1347
|
+
const follow = paintOnly ? 0 : reducedMotion ? 1 : 0.035
|
|
1326
1348
|
glowX += (center.x - glowX) * follow
|
|
1327
1349
|
glowY += (center.y - glowY) * follow
|
|
1328
1350
|
if (glow) glow.style.transform = 'translate3d(calc(' + glowX.toFixed(2) + 'px - 50%), calc(' + glowY.toFixed(2) + 'px - 50%), 0)'
|
|
@@ -1337,19 +1359,31 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1337
1359
|
const damping = heroPhysics ? 0.86 : 0.84
|
|
1338
1360
|
const pointerForce = heroPhysics ? 2.2 : 2.7
|
|
1339
1361
|
const lightScheme = document.documentElement.dataset.prtsScheme === 'light'
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1362
|
+
const contrast = heroContrastStrength()
|
|
1363
|
+
if (contrast > 0 && context.createRadialGradient) {
|
|
1364
|
+
const origin = heroActive ? center : transition.fromCenter
|
|
1365
|
+
const radius = (heroActive ? fieldSize : transition.fromSize) * .78
|
|
1366
|
+
const shade = lightScheme ? '248,250,252' : '7,14,22'
|
|
1367
|
+
const gradient = context.createRadialGradient(origin.x, origin.y, 0, origin.x, origin.y, radius)
|
|
1368
|
+
gradient.addColorStop(0, `rgba(${shade},${.58 * contrast})`)
|
|
1369
|
+
gradient.addColorStop(.42, `rgba(${shade},${.42 * contrast})`)
|
|
1370
|
+
gradient.addColorStop(1, `rgba(${shade},0)`)
|
|
1371
|
+
context.fillStyle = gradient
|
|
1372
|
+
context.fillRect(origin.x - radius, origin.y - radius, radius * 2, radius * 2)
|
|
1373
|
+
}
|
|
1374
|
+
const baseAlpha = (lightScheme ? 0.66 : 0.72) + .16 * contrast
|
|
1375
|
+
const radiusScale = (lightScheme ? 1.12 : 1) * (1 + .08 * contrast)
|
|
1342
1376
|
const particleBatches = typeof context.moveTo === 'function' ? new Map() : null
|
|
1343
1377
|
|
|
1344
1378
|
for (const particle of particles) {
|
|
1345
1379
|
const target = targetPosition(particle, frameGeometry)
|
|
1346
|
-
if (staticField) {
|
|
1380
|
+
if (staticField && !paintOnly) {
|
|
1347
1381
|
particle.x = target.x
|
|
1348
1382
|
particle.y = target.y
|
|
1349
1383
|
particle.vx = 0
|
|
1350
1384
|
particle.vy = 0
|
|
1351
1385
|
}
|
|
1352
|
-
if (!staticField) {
|
|
1386
|
+
if (!staticField && !paintOnly) {
|
|
1353
1387
|
particle.vx += (target.x - particle.x) * spring
|
|
1354
1388
|
particle.vy += (target.y - particle.y) * spring
|
|
1355
1389
|
|
|
@@ -1412,6 +1446,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1412
1446
|
canvas.setAttribute('data-prts-particle-ready', '')
|
|
1413
1447
|
canvas.style.visibility = ''
|
|
1414
1448
|
}
|
|
1449
|
+
if (paintOnly) return
|
|
1415
1450
|
const explicitMotion = Boolean(densityTransition || transition)
|
|
1416
1451
|
const animatedField = !staticField && context && particles.length
|
|
1417
1452
|
if (explicitMotion) {
|
|
@@ -1550,7 +1585,15 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1550
1585
|
}
|
|
1551
1586
|
transitionStartedAt = undefined
|
|
1552
1587
|
pointerActive = false
|
|
1553
|
-
|
|
1588
|
+
// The initial message may have paused hydration before syncHeroState ran.
|
|
1589
|
+
// Let this short exit finish uninterrupted by subsequent response updates.
|
|
1590
|
+
cancelHydrationResume()
|
|
1591
|
+
hydrationPaused = false
|
|
1592
|
+
mutationBurstScore = 0
|
|
1593
|
+
if (mutationBurstTimer !== undefined) window.clearTimeout?.(mutationBurstTimer)
|
|
1594
|
+
mutationBurstTimer = undefined
|
|
1595
|
+
if (pendingResizeSize) resizeHeroTransition(pendingResizeSize)
|
|
1596
|
+
updateFrameGate()
|
|
1554
1597
|
}
|
|
1555
1598
|
|
|
1556
1599
|
function syncHeroState() {
|
|
@@ -1612,7 +1655,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1612
1655
|
}
|
|
1613
1656
|
|
|
1614
1657
|
function registerContentMutations(mutations) {
|
|
1615
|
-
if (!fieldEnabled() || phoneStatic()) return
|
|
1658
|
+
if (!fieldEnabled() || phoneStatic() || transition?.fromHero) return
|
|
1616
1659
|
let weight = mutations.length
|
|
1617
1660
|
for (const mutation of mutations) {
|
|
1618
1661
|
for (const node of [...mutation.addedNodes, ...mutation.removedNodes]) {
|
|
@@ -1780,6 +1823,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1780
1823
|
if (context && particles.length) {
|
|
1781
1824
|
syncCanvasBackingStore()
|
|
1782
1825
|
geometryDirty = false
|
|
1826
|
+
cancelFrame()
|
|
1783
1827
|
draw(Number(window.performance?.now?.()) || 0)
|
|
1784
1828
|
} else {
|
|
1785
1829
|
canvas.setAttribute('data-prts-particle-ready', '')
|
|
@@ -1847,6 +1891,11 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1847
1891
|
startupPaused = next
|
|
1848
1892
|
updateFrameGate()
|
|
1849
1893
|
},
|
|
1894
|
+
repaintScheme() {
|
|
1895
|
+
cancelFrame()
|
|
1896
|
+
draw(Number(window.performance?.now?.()) || 0, true)
|
|
1897
|
+
if (!schemeTransitionPaused) scheduleFrame()
|
|
1898
|
+
},
|
|
1850
1899
|
setSchemeTransitionActive(active) {
|
|
1851
1900
|
const next = Boolean(active)
|
|
1852
1901
|
if (schemeTransitionPaused === next) return
|
|
@@ -1888,10 +1937,13 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1888
1937
|
|
|
1889
1938
|
findMount()
|
|
1890
1939
|
if (!fieldEnabled()) resetTraversal()
|
|
1891
|
-
const
|
|
1940
|
+
const previousRequestedDensity = heroActive
|
|
1892
1941
|
? previousPreferences.heroParticleDensity
|
|
1893
1942
|
: previousPreferences.conversationParticleDensity
|
|
1894
|
-
|
|
1943
|
+
// Compare effective densities on both sides: sparse/light hero settings
|
|
1944
|
+
// already render at standard density, including after a focus refresh.
|
|
1945
|
+
const previousActiveDensity = heroActive ? effectiveHeroDensity(previousRequestedDensity) : previousRequestedDensity
|
|
1946
|
+
const activeDensityChanged = previousRequestedDensity !== undefined && previousActiveDensity !== activeDensity()
|
|
1895
1947
|
reportState({ requestedPattern: 'orthogonal' })
|
|
1896
1948
|
if (particles.length && activeDensityChanged) scheduleDensityReassembly(targetParticleCount(), true)
|
|
1897
1949
|
if (preservedTraversalCoordinate !== undefined && scroller && !heroActive) reanchorTraversal(preservedTraversalCoordinate)
|
|
@@ -1929,6 +1981,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1929
1981
|
enabled: fieldEnabled(),
|
|
1930
1982
|
mounted: Boolean(ambient?.isConnected && canvas?.isConnected),
|
|
1931
1983
|
hero: heroActive,
|
|
1984
|
+
heroContrast: heroContrastStrength(),
|
|
1932
1985
|
side,
|
|
1933
1986
|
emblemKey: (heroActive || transition?.fromHero ? heroEmblem() : emblemForSegment(segment))?.key,
|
|
1934
1987
|
nextEmblemKey: transition ? emblemForSegment(transition.targetSegment)?.key : null,
|
|
@@ -27,6 +27,7 @@ export const DEFAULT_PREFERENCES = Object.freeze({
|
|
|
27
27
|
composerGlass: false,
|
|
28
28
|
backgroundMode: 'off',
|
|
29
29
|
backgroundDim: .45,
|
|
30
|
+
heroContrast: true,
|
|
30
31
|
sidebarOpacity: .06,
|
|
31
32
|
railOpacity: .3,
|
|
32
33
|
bakerOpacity: .5,
|
|
@@ -42,7 +43,9 @@ export const DEFAULT_PREFERENCES = Object.freeze({
|
|
|
42
43
|
particlePattern: 'orthogonal',
|
|
43
44
|
railDefaultHidden: false,
|
|
44
45
|
sessionPreview: true,
|
|
46
|
+
aiToolsEnabled: true,
|
|
45
47
|
previewOngoing: true,
|
|
48
|
+
previewTools: true,
|
|
46
49
|
previewWarning: true,
|
|
47
50
|
previewDone: true,
|
|
48
51
|
previewFollowOngoing: true,
|
|
@@ -97,11 +100,12 @@ export const PARTICLE_DETAIL_LEVELS = Object.freeze({
|
|
|
97
100
|
precise: Object.freeze({ conversationParticleDensity: 'dense', heroParticleDensity: 'ultra' }),
|
|
98
101
|
})
|
|
99
102
|
export const PREFERENCE_GROUPS = Object.freeze({
|
|
100
|
-
background: Object.freeze(['texture', 'backgroundMode', 'backgroundDim', 'sidebarOpacity', 'railOpacity', 'bakerOpacity', 'classicUserOpacity', 'sidebarBlur', 'railBlur', 'bakerBlur', 'classicUserBlur']),
|
|
103
|
+
background: Object.freeze(['texture', 'backgroundMode', 'backgroundDim', 'heroContrast', 'sidebarOpacity', 'railOpacity', 'bakerOpacity', 'classicUserOpacity', 'sidebarBlur', 'railBlur', 'bakerBlur', 'classicUserBlur']),
|
|
101
104
|
particles: Object.freeze(['conversationParticleDensity', 'heroParticleDensity', 'particleTraversalSpeed']),
|
|
102
105
|
material: Object.freeze(['glass', 'composerGlass']),
|
|
103
106
|
accessibility: Object.freeze(['motion', 'bootAnimation', 'cursor']),
|
|
104
|
-
|
|
107
|
+
aiTools: Object.freeze(['aiToolsEnabled']),
|
|
108
|
+
navigation: Object.freeze(['railDefaultHidden', 'sessionPreview', 'previewOngoing', 'previewTools', 'previewWarning', 'previewDone', 'previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning', 'previewReplyLimit']),
|
|
105
109
|
conversation: Object.freeze(['sessionFlow', 'sessionGlow', 'sessionFlowPalette', 'sessionFlowColorBack', 'sessionFlowColorCore', 'sessionFlowColorFront', 'sessionFlowSpeed', 'conversationStyle', 'bakerGroupAvatars', 'bakerLoading', 'conversationEmblem']),
|
|
106
110
|
})
|
|
107
111
|
|
|
@@ -217,6 +221,7 @@ export function normalizePreferences(value) {
|
|
|
217
221
|
railBlur: materialBlurOr(input.railBlur, DEFAULT_PREFERENCES.railBlur),
|
|
218
222
|
bakerBlur: materialBlurOr(input.bakerBlur, DEFAULT_PREFERENCES.bakerBlur),
|
|
219
223
|
classicUserBlur: materialBlurOr(input.classicUserBlur, DEFAULT_PREFERENCES.classicUserBlur),
|
|
224
|
+
heroContrast: booleanOr(input.heroContrast, DEFAULT_PREFERENCES.heroContrast),
|
|
220
225
|
backgroundDim: typeof input.backgroundDim === 'number' && Number.isFinite(input.backgroundDim) ? Math.min(.9, Math.max(0, input.backgroundDim)) : .45,
|
|
221
226
|
composerGlass: booleanOr(input.composerGlass, DEFAULT_PREFERENCES.composerGlass),
|
|
222
227
|
bootAnimation: booleanOr(input.bootAnimation, DEFAULT_PREFERENCES.bootAnimation),
|
|
@@ -237,7 +242,9 @@ export function normalizePreferences(value) {
|
|
|
237
242
|
sessionFlowColorFront: sessionFlowColors.front,
|
|
238
243
|
sessionFlowSpeed: sessionFlowSpeedOr(input.sessionFlowSpeed, DEFAULT_PREFERENCES.sessionFlowSpeed),
|
|
239
244
|
sessionPreview: booleanOr(input.sessionPreview, DEFAULT_PREFERENCES.sessionPreview),
|
|
245
|
+
aiToolsEnabled: booleanOr(input.aiToolsEnabled, DEFAULT_PREFERENCES.aiToolsEnabled),
|
|
240
246
|
previewOngoing: booleanOr(input.previewOngoing, DEFAULT_PREFERENCES.previewOngoing),
|
|
247
|
+
previewTools: booleanOr(input.previewTools, DEFAULT_PREFERENCES.previewTools),
|
|
241
248
|
previewWarning: booleanOr(input.previewWarning, DEFAULT_PREFERENCES.previewWarning),
|
|
242
249
|
previewDone: booleanOr(input.previewDone, DEFAULT_PREFERENCES.previewDone),
|
|
243
250
|
previewReplyLimit: [0, 5, 10, '0', '5', '10'].includes(input.previewReplyLimit) ? Number(input.previewReplyLimit) : 5,
|
|
@@ -39,6 +39,60 @@ export function latestAssistantReply(snapshot) {
|
|
|
39
39
|
return ''
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// Call objects may be mutable on older hosts; compare raw arguments before reuse.
|
|
43
|
+
const toolSummaryCache = new WeakMap()
|
|
44
|
+
function toolSummary(call) {
|
|
45
|
+
const raw = call.argsRaw ?? call.call?.argsRaw ?? ''
|
|
46
|
+
const cached = toolSummaryCache.get(call)
|
|
47
|
+
if (cached?.raw === raw) return cached.text
|
|
48
|
+
let args
|
|
49
|
+
try { args = JSON.parse(raw) } catch {}
|
|
50
|
+
const summary = ['description', 'command', 'cmd', 'file_path', 'filePath', 'path', 'query', 'pattern', 'url']
|
|
51
|
+
.map(field => args?.[field]).find(value => typeof value === 'string' && value.trim())
|
|
52
|
+
const normalized = summary?.replace(/\s+/g, ' ').trim()
|
|
53
|
+
const text = normalized ? normalized.slice(0, 160) + (normalized.length > 160 ? '…' : '') : ''
|
|
54
|
+
toolSummaryCache.set(call, {raw, text})
|
|
55
|
+
return text
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Only calls after the latest settled prose belong to the current progress preview.
|
|
59
|
+
export function previewToolCalls(snapshot) {
|
|
60
|
+
const turn = snapshot?.timeline?.turnOrder?.at(-1)
|
|
61
|
+
const order = snapshot?.order ?? []
|
|
62
|
+
const calls = [], seen = new Set()
|
|
63
|
+
const visit = (call, key) => {
|
|
64
|
+
if (!call) return
|
|
65
|
+
const id = call.callId ?? key
|
|
66
|
+
if (!seen.has(id)) { seen.add(id); calls.push({call, key: id}) }
|
|
67
|
+
for (const [index, child] of (call.subCalls ?? []).entries()) visit(child, key + ':' + index)
|
|
68
|
+
}
|
|
69
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
70
|
+
const node = snapshot.nodes?.get?.(order[i])
|
|
71
|
+
if (['user', 'steering', 'turn-trigger'].includes(node?.kind)) break
|
|
72
|
+
if (turn !== undefined && node?.location?.turn?.turn !== turn) continue
|
|
73
|
+
if (node?.visibility === 'hidden') continue
|
|
74
|
+
if (node?.kind === 'assistant-step' && node.data?.status === 'settled'
|
|
75
|
+
&& node.data.blocks?.some(block => block.kind === 'text' && block.text?.trim())) break
|
|
76
|
+
if (node?.kind === 'tool-call') visit(node.data?.root, order[i])
|
|
77
|
+
}
|
|
78
|
+
return calls.sort((a, b) => (b.call.callTime ?? b.call.time ?? 0) - (a.call.callTime ?? a.call.time ?? 0)).map(({call, key}) => {
|
|
79
|
+
const text = toolSummary(call)
|
|
80
|
+
return {
|
|
81
|
+
key,
|
|
82
|
+
name: call.name || call.call?.name || '工具调用',
|
|
83
|
+
status: call.kind === 'tool-result' ? call.isError ? '失败' : '已完成'
|
|
84
|
+
: call.phase === 'preparing' ? '准备中' : '执行中',
|
|
85
|
+
summary: text ? text
|
|
86
|
+
: call.phase === 'preparing' ? '正在准备调用…' : '暂无操作摘要',
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function latestToolCall(snapshot) {
|
|
92
|
+
const records = previewToolCalls(snapshot)
|
|
93
|
+
return records.length ? {...records[0], records} : undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
42
96
|
export function completedPreviewReplies(snapshot) {
|
|
43
97
|
const replies = []
|
|
44
98
|
const latestTurn = snapshot?.timeline?.turnOrder?.at(-1)
|
|
@@ -142,8 +196,8 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
142
196
|
active = undefined
|
|
143
197
|
if (!old) return
|
|
144
198
|
window.clearTimeout(old.leaveTimer)
|
|
145
|
-
window.
|
|
146
|
-
window.
|
|
199
|
+
window.cancelAnimationFrame(old.delay)
|
|
200
|
+
window.cancelAnimationFrame(old.refresh)
|
|
147
201
|
old.unsubscribe?.()
|
|
148
202
|
old.unsubscribeStatus?.()
|
|
149
203
|
if (keepWarm && old.reference && old.source) {
|
|
@@ -311,6 +365,39 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
311
365
|
body.setAttribute('data-prts-preview-body', '')
|
|
312
366
|
panel.querySelector('p').replaceWith(body)
|
|
313
367
|
}
|
|
368
|
+
panel.querySelector('[data-prts-preview-tool]')?.remove()
|
|
369
|
+
const toolSection = panel.ownerDocument.createElement('div')
|
|
370
|
+
toolSection.setAttribute('data-prts-preview-tool', '')
|
|
371
|
+
toolSection.hidden = true
|
|
372
|
+
body.before(toolSection)
|
|
373
|
+
let previousTool, currentTool
|
|
374
|
+
const toolEntry = panel.ownerDocument.createElement('button')
|
|
375
|
+
toolEntry.type = 'button'
|
|
376
|
+
toolEntry.setAttribute('data-prts-tool-entry', '')
|
|
377
|
+
const toolHeading = panel.ownerDocument.createElement('small')
|
|
378
|
+
const toolName = panel.ownerDocument.createElement('span')
|
|
379
|
+
toolEntry.append(toolHeading, toolName)
|
|
380
|
+
toolSection.append(toolEntry)
|
|
381
|
+
const openToolDetail = (refresh = false) => {
|
|
382
|
+
if (currentTool) showDetail({key: 'latest-tool', tool: currentTool}, 0, toolEntry, refresh)
|
|
383
|
+
}
|
|
384
|
+
toolEntry.addEventListener('pointermove', () => openToolDetail())
|
|
385
|
+
toolEntry.addEventListener('focus', () => openToolDetail())
|
|
386
|
+
toolEntry.addEventListener('click', () => openToolDetail())
|
|
387
|
+
const renderTool = tool => {
|
|
388
|
+
const signature = JSON.stringify(tool)
|
|
389
|
+
if (signature === previousTool) return
|
|
390
|
+
previousTool = signature
|
|
391
|
+
currentTool = tool
|
|
392
|
+
toolSection.hidden = !tool
|
|
393
|
+
if (!tool) {
|
|
394
|
+
if (detailKey === 'latest-tool') hideDetail()
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
toolHeading.textContent = '工具 · ' + tool.status + ' · ' + tool.records.length + ' 次'
|
|
398
|
+
toolName.textContent = tool.name
|
|
399
|
+
if (detailKey === 'latest-tool') openToolDetail(true)
|
|
400
|
+
}
|
|
314
401
|
const choices = panel.ownerDocument.createElement('div')
|
|
315
402
|
choices.setAttribute('data-prts-preview-choices', '')
|
|
316
403
|
panel.querySelector('[data-prts-preview-choices]')?.remove()
|
|
@@ -450,9 +537,9 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
450
537
|
detailEntry?.removeAttribute('data-prts-reply-active')
|
|
451
538
|
detailEntry = undefined
|
|
452
539
|
}
|
|
453
|
-
const showDetail = (reply, index, item) => {
|
|
540
|
+
const showDetail = (reply, index, item, refresh = false) => {
|
|
454
541
|
keep()
|
|
455
|
-
if (detailKey === reply.key && current.detail && !current.detail.hidden) return
|
|
542
|
+
if (!refresh && detailKey === reply.key && current.detail && !current.detail.hidden) return
|
|
456
543
|
if (!current.detail) {
|
|
457
544
|
const detail = panel.ownerDocument.createElement('section')
|
|
458
545
|
detail.setAttribute('data-prts-summary-reply', '')
|
|
@@ -472,10 +559,36 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
472
559
|
detailEntry?.removeAttribute('data-prts-reply-active')
|
|
473
560
|
detailEntry = item
|
|
474
561
|
item?.setAttribute('data-prts-reply-active', '')
|
|
475
|
-
detail.
|
|
562
|
+
detail.toggleAttribute('data-prts-tool-history', Boolean(reply.tool))
|
|
563
|
+
detail.querySelector('strong').textContent = reply.tool ? '本段工具记录 · ' + reply.tool.records.length + ' 次' : '回复 ' + (reply.ordinal ?? index + 1)
|
|
476
564
|
const content = detail.querySelector('[data-prts-preview-body]')
|
|
477
|
-
content.
|
|
478
|
-
|
|
565
|
+
const scrollTop = refresh ? content.scrollTop : 0
|
|
566
|
+
let anchorKey, anchorOffset
|
|
567
|
+
if (refresh && scrollTop > 0) {
|
|
568
|
+
const anchor = [...content.children].find(row => row.getBoundingClientRect().bottom > content.getBoundingClientRect().top)
|
|
569
|
+
anchorKey = anchor?.dataset.toolKey
|
|
570
|
+
anchorOffset = anchor?.getBoundingClientRect().top
|
|
571
|
+
}
|
|
572
|
+
if (reply.tool) {
|
|
573
|
+
content.replaceChildren(...reply.tool.records.map(record => {
|
|
574
|
+
const row = panel.ownerDocument.createElement('article')
|
|
575
|
+
row.setAttribute('data-prts-tool-record', '')
|
|
576
|
+
row.dataset.toolKey = record.key
|
|
577
|
+
const heading = panel.ownerDocument.createElement('div')
|
|
578
|
+
const name = panel.ownerDocument.createElement('b')
|
|
579
|
+
name.textContent = record.name
|
|
580
|
+
const status = panel.ownerDocument.createElement('small')
|
|
581
|
+
status.textContent = record.status
|
|
582
|
+
heading.append(name, status)
|
|
583
|
+
const summary = panel.ownerDocument.createElement('p')
|
|
584
|
+
summary.textContent = record.summary
|
|
585
|
+
row.append(heading, summary)
|
|
586
|
+
return row
|
|
587
|
+
}))
|
|
588
|
+
} else content.innerHTML = current.contentCache(reply.text).html
|
|
589
|
+
content.scrollTop = scrollTop
|
|
590
|
+
const retainedAnchor = anchorKey && [...content.children].find(row => row.dataset.toolKey === anchorKey)
|
|
591
|
+
if (retainedAnchor) content.scrollTop += retainedAnchor.getBoundingClientRect().top - anchorOffset
|
|
479
592
|
detail.hidden = false
|
|
480
593
|
const rect = panel.getBoundingClientRect()
|
|
481
594
|
const width = Math.min(340, window.innerWidth - 16)
|
|
@@ -548,11 +661,18 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
548
661
|
}))
|
|
549
662
|
}
|
|
550
663
|
choices.hidden = !labels.length
|
|
664
|
+
const tool = state === 'ongoing' && !question && getPreviewPreferences?.().previewTools !== false ? latestToolCall(source?.getSnapshot()) : undefined
|
|
665
|
+
renderTool(tool)
|
|
666
|
+
if (tool) label.textContent = '运行中 · 最新活动'
|
|
551
667
|
panel.setAttribute('aria-label', title + ':' + label.textContent)
|
|
552
668
|
if (state === 'ongoing' && source) {
|
|
553
669
|
const replies = selectReplies(source.getSnapshot(), replyLimit())
|
|
554
|
-
|
|
555
|
-
|
|
670
|
+
panel.toggleAttribute('data-prts-preview-empty', !replies.length && !tool)
|
|
671
|
+
if (replies === historyKeys) {
|
|
672
|
+
const emptyText = tool ? '' : '正在处理…'
|
|
673
|
+
if (!replies.length && body.textContent !== emptyText) body.textContent = emptyText
|
|
674
|
+
return
|
|
675
|
+
}
|
|
556
676
|
const previousTop = body.scrollTop
|
|
557
677
|
const scrollAnchor = previousTop > 0 ? [...body.children].find(entry => entry.getBoundingClientRect().bottom > body.getBoundingClientRect().top) : undefined
|
|
558
678
|
const anchorTop = scrollAnchor?.getBoundingClientRect().top
|
|
@@ -568,7 +688,7 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
568
688
|
}
|
|
569
689
|
}
|
|
570
690
|
if (!historyKeys.length) body.replaceChildren()
|
|
571
|
-
if (!replies.length) body.textContent = '
|
|
691
|
+
if (!replies.length) body.textContent = tool ? '' : '正在处理…'
|
|
572
692
|
for (let index = 0; index < replies.length; index++) {
|
|
573
693
|
const reply = replies[index]
|
|
574
694
|
if (retained.has(reply.key)) continue
|
|
@@ -580,6 +700,13 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
580
700
|
item.setAttribute('data-prts-reply-entry', '')
|
|
581
701
|
const heading = panel.ownerDocument.createElement('small')
|
|
582
702
|
heading.textContent = '回复 ' + reply.ordinal
|
|
703
|
+
const icon = panel.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
704
|
+
icon.setAttribute('viewBox', '0 0 16 16')
|
|
705
|
+
icon.setAttribute('aria-hidden', 'true')
|
|
706
|
+
const outline = panel.ownerDocument.createElementNS(icon.namespaceURI, 'path')
|
|
707
|
+
outline.setAttribute('d', 'M3 2.5h10a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7l-4 2v-2a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1Z')
|
|
708
|
+
icon.append(outline)
|
|
709
|
+
heading.prepend(icon)
|
|
583
710
|
const summary = panel.ownerDocument.createElement('span')
|
|
584
711
|
summary.textContent = current.contentCache(reply.text).summary
|
|
585
712
|
item.append(heading, summary)
|
|
@@ -610,7 +737,7 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
610
737
|
? approval
|
|
611
738
|
? [pending.toolName, pending.displayReason?.['zh-CN'] || pending.displayReason?.zh || pending.reason || pending.displayReason?.en || '请进入会话查看操作详情'].filter(Boolean).join(':')
|
|
612
739
|
: first?.question || pendingQuestionPreview(pending)
|
|
613
|
-
: source ? excerpt || (state === 'ongoing' ? '
|
|
740
|
+
: source ? excerpt || (state === 'ongoing' ? '正在处理…' : '暂无正文回复,可进入会话查看执行记录。') : '正在读取…'
|
|
614
741
|
if (previousText !== text || previousQuestion !== question) {
|
|
615
742
|
const extendsReply = previousQuestion === question && previousText && text.startsWith(previousText)
|
|
616
743
|
if (question) body.textContent = text
|
|
@@ -631,7 +758,8 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
631
758
|
})
|
|
632
759
|
render()
|
|
633
760
|
body.scrollTop = 0
|
|
634
|
-
current.delay = window.
|
|
761
|
+
current.delay = window.requestAnimationFrame(async () => {
|
|
762
|
+
current.delay = undefined
|
|
635
763
|
try {
|
|
636
764
|
const service = getUiConversation?.()
|
|
637
765
|
const controller = getSessions?.() ?? sessions
|
|
@@ -643,11 +771,13 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
643
771
|
current.source = source
|
|
644
772
|
current.unsubscribe = source.subscribe(() => {
|
|
645
773
|
if (active !== current || current.refresh !== undefined) return
|
|
646
|
-
current.refresh = window.
|
|
774
|
+
current.refresh = window.requestAnimationFrame(() => {
|
|
647
775
|
current.refresh = undefined
|
|
648
|
-
if (current.state === 'ongoing' && !current.editor
|
|
776
|
+
if (current.state === 'ongoing' && !current.editor
|
|
777
|
+
&& selectReplies(source.getSnapshot(), replyLimit()) === historyKeys
|
|
778
|
+
&& JSON.stringify(getPreviewPreferences?.().previewTools === false ? undefined : latestToolCall(source.getSnapshot())) === previousTool) return
|
|
649
779
|
render()
|
|
650
|
-
}
|
|
780
|
+
})
|
|
651
781
|
})
|
|
652
782
|
render()
|
|
653
783
|
} catch {
|
|
@@ -659,7 +789,7 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
659
789
|
render()
|
|
660
790
|
if (body.textContent.trim() === '正在读取…') body.textContent = '暂时无法读取回复,可点击会话查看'
|
|
661
791
|
}
|
|
662
|
-
}
|
|
792
|
+
})
|
|
663
793
|
}
|
|
664
794
|
function leave() {
|
|
665
795
|
if (!active || active.sticky) return
|
|
@@ -673,6 +803,8 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
673
803
|
if (!active) return
|
|
674
804
|
const anchor = [...root.querySelectorAll('[data-prts-session-summary-item]')]
|
|
675
805
|
.find(item => item.getAttribute('data-prts-session-summary-item') === active.id)
|
|
806
|
+
// A read completion (or removed session) has no status row to own a preview.
|
|
807
|
+
if (!anchor) { stop(); return }
|
|
676
808
|
active.group?.removeAttribute('data-prts-preview-open')
|
|
677
809
|
active.anchor?.removeAttribute('data-prts-preview-active')
|
|
678
810
|
active.anchor = anchor
|