@ash-qw/dsh-theme-prts 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/lib/client.js +298 -57
- package/package.json +1 -1
- package/src/client/preferences.js +7 -1
- package/src/client/preview-markdown.js +26 -0
- package/src/client/session-summary-preview.js +158 -38
- package/src/client/sidebar-control-adapter.js +105 -16
- package/src/client/theme-settings-workbench.js +15 -1
- package/src/styles/facility-vector.css +5 -0
- package/src/styles/theme-settings-workbench.css +7 -0
package/package.json
CHANGED
|
@@ -34,7 +34,10 @@ export const DEFAULT_PREFERENCES = Object.freeze({
|
|
|
34
34
|
previewOngoing: true,
|
|
35
35
|
previewWarning: true,
|
|
36
36
|
previewDone: true,
|
|
37
|
+
previewFollowOngoing: true,
|
|
38
|
+
previewFollowWarning: false,
|
|
37
39
|
previewDoubleClick: true,
|
|
40
|
+
previewReplyLimit: 5,
|
|
38
41
|
|
|
39
42
|
sessionFlow: true,
|
|
40
43
|
sessionGlow: true,
|
|
@@ -86,7 +89,7 @@ export const PREFERENCE_GROUPS = Object.freeze({
|
|
|
86
89
|
particles: Object.freeze(['conversationParticleDensity', 'heroParticleDensity', 'particleTraversalSpeed']),
|
|
87
90
|
material: Object.freeze(['glass']),
|
|
88
91
|
accessibility: Object.freeze(['motion', 'bootAnimation', 'cursor']),
|
|
89
|
-
navigation: Object.freeze(['railDefaultHidden', 'sessionPreview', 'previewOngoing', 'previewWarning', 'previewDone', 'previewDoubleClick']),
|
|
92
|
+
navigation: Object.freeze(['railDefaultHidden', 'sessionPreview', 'previewOngoing', 'previewWarning', 'previewDone', 'previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning', 'previewReplyLimit']),
|
|
90
93
|
conversation: Object.freeze(['sessionFlow', 'sessionGlow', 'sessionFlowPalette', 'sessionFlowColorBack', 'sessionFlowColorCore', 'sessionFlowColorFront', 'sessionFlowSpeed', 'conversationStyle', 'bakerGroupAvatars', 'conversationEmblem']),
|
|
91
94
|
})
|
|
92
95
|
|
|
@@ -207,6 +210,9 @@ export function normalizePreferences(value) {
|
|
|
207
210
|
previewOngoing: booleanOr(input.previewOngoing, DEFAULT_PREFERENCES.previewOngoing),
|
|
208
211
|
previewWarning: booleanOr(input.previewWarning, DEFAULT_PREFERENCES.previewWarning),
|
|
209
212
|
previewDone: booleanOr(input.previewDone, DEFAULT_PREFERENCES.previewDone),
|
|
213
|
+
previewReplyLimit: [0, 5, 10, '0', '5', '10'].includes(input.previewReplyLimit) ? Number(input.previewReplyLimit) : 5,
|
|
214
|
+
previewFollowOngoing: booleanOr(input.previewFollowOngoing, booleanOr(input.previewFollowState, true)),
|
|
215
|
+
previewFollowWarning: booleanOr(input.previewFollowWarning, booleanOr(input.previewFollowState, false)),
|
|
210
216
|
previewDoubleClick: booleanOr(input.previewDoubleClick, DEFAULT_PREFERENCES.previewDoubleClick),
|
|
211
217
|
conversationEmblem: booleanOr(input.conversationEmblem, DEFAULT_PREFERENCES.conversationEmblem),
|
|
212
218
|
bakerGroupAvatars: booleanOr(input.bakerGroupAvatars, DEFAULT_PREFERENCES.bakerGroupAvatars),
|
|
@@ -16,3 +16,29 @@ export function renderPreviewMarkdown(text) {
|
|
|
16
16
|
}
|
|
17
17
|
return previewMarkdownParser.render(text)
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
// Bounded per retained session; oversized replies are rendered without caching.
|
|
21
|
+
export function createPreviewContentCache(document, render = renderPreviewMarkdown) {
|
|
22
|
+
const entries = new Map()
|
|
23
|
+
let size = 0
|
|
24
|
+
return text => {
|
|
25
|
+
const hit = entries.get(text)
|
|
26
|
+
if (hit) { entries.delete(text); entries.set(text, hit); return hit }
|
|
27
|
+
const html = render(text)
|
|
28
|
+
const plain = document.createElement('div')
|
|
29
|
+
plain.innerHTML = html
|
|
30
|
+
const summary = plain.textContent.replace(/\s+/g, ' ').trim().slice(0, 100)
|
|
31
|
+
const cost = text.length + html.length + summary.length
|
|
32
|
+
const value = {html, summary, cost}
|
|
33
|
+
if (cost <= 262144) {
|
|
34
|
+
while (entries.size && (entries.size >= 32 || size + cost > 262144)) {
|
|
35
|
+
const key = entries.keys().next().value
|
|
36
|
+
size -= entries.get(key).cost
|
|
37
|
+
entries.delete(key)
|
|
38
|
+
}
|
|
39
|
+
entries.set(text, value)
|
|
40
|
+
size += cost
|
|
41
|
+
}
|
|
42
|
+
return value
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createPreviewContentCache } from './preview-markdown.js'
|
|
2
2
|
export function canAnswerPreview(pending) {
|
|
3
3
|
if (pending?.kind === 'approval') return pending.answerable === true && typeof pending.answer === 'function'
|
|
4
4
|
const question = pending?.questions?.[0]
|
|
@@ -42,7 +42,9 @@ export function latestAssistantReply(snapshot) {
|
|
|
42
42
|
export function completedPreviewReplies(snapshot) {
|
|
43
43
|
const replies = []
|
|
44
44
|
const latestTurn = snapshot?.timeline?.turnOrder?.at(-1)
|
|
45
|
-
|
|
45
|
+
const order = snapshot?.order ?? []
|
|
46
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
47
|
+
const key = order[i]
|
|
46
48
|
const node = snapshot.nodes?.get?.(key)
|
|
47
49
|
if (['user', 'steering', 'turn-trigger'].includes(node?.kind)) break
|
|
48
50
|
if (latestTurn !== undefined && node?.location?.turn?.turn !== latestTurn) continue
|
|
@@ -53,10 +55,71 @@ export function completedPreviewReplies(snapshot) {
|
|
|
53
55
|
return replies.reverse()
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
// Ignore streaming text: only settled prose and turn boundaries affect this list.
|
|
59
|
+
// Cache immutable host data; mutable test/older-host data is read on each check.
|
|
60
|
+
export function createPreviewReplySelector() {
|
|
61
|
+
const textCache = new WeakMap()
|
|
62
|
+
let previous = [], previousTurn, previousBoundary
|
|
63
|
+
return (snapshot, limit = 5) => {
|
|
64
|
+
const order = snapshot?.order ?? []
|
|
65
|
+
const turn = snapshot?.timeline?.turnOrder?.at(-1)
|
|
66
|
+
const replies = []
|
|
67
|
+
let boundary
|
|
68
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
69
|
+
const key = order[i], node = snapshot.nodes?.get?.(key)
|
|
70
|
+
if (['user', 'steering', 'turn-trigger'].includes(node?.kind)) { boundary = key; break }
|
|
71
|
+
if (turn !== undefined && node?.location?.turn?.turn !== turn) continue
|
|
72
|
+
if (node?.kind !== 'assistant-step' || node.visibility === 'hidden' || node.data?.status !== 'settled') continue
|
|
73
|
+
let text = textCache.get(node.data)
|
|
74
|
+
if (text === undefined) {
|
|
75
|
+
text = (node.data.blocks ?? []).filter(block => block.kind === 'text').map(block => block.text ?? '').join('').trim()
|
|
76
|
+
if (Object.isFrozen(node.data)) textCache.set(node.data, text)
|
|
77
|
+
}
|
|
78
|
+
if (text) replies.push({key, text})
|
|
79
|
+
}
|
|
80
|
+
replies.reverse()
|
|
81
|
+
const selected = (limit > 0 ? replies.slice(-limit) : replies)
|
|
82
|
+
.map((reply, index) => ({...reply, ordinal: replies.length - (limit > 0 ? Math.min(limit, replies.length) : replies.length) + index + 1}))
|
|
83
|
+
if (turn === previousTurn && boundary === previousBoundary && selected.length === previous.length && selected.every((reply, i) => reply.key === previous[i].key && reply.text === previous[i].text && reply.ordinal === previous[i].ordinal)) return previous
|
|
84
|
+
previousTurn = turn
|
|
85
|
+
previousBoundary = boundary
|
|
86
|
+
return previous = selected
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function previewStatusFields(status) {
|
|
91
|
+
const pending = status?.pendingInteraction
|
|
92
|
+
return [status?.running, status?.completionUnread, pending, pending?.answer,
|
|
93
|
+
JSON.stringify([pending?.kind, pending?.answerable, pending?.getSnapshot?.().closed,
|
|
94
|
+
pending?.questions, pending?.toolName, pending?.reason, pending?.displayReason])]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createSessionSummaryPreview({ document, window, sessions, getSessions, getUiConversation, getSessionStatus, openSession, getPreviewPreferences, onPreviewClosed, onAnswerSubmitted }) {
|
|
57
98
|
let active
|
|
58
|
-
function
|
|
59
|
-
|
|
99
|
+
function follows(state) {
|
|
100
|
+
const preferences = getPreviewPreferences?.() ?? {}
|
|
101
|
+
return state === 'warning' ? preferences.previewFollowWarning === true : preferences.previewFollowOngoing !== false
|
|
102
|
+
}
|
|
103
|
+
// Shared across running, pending and completed previews. Active ownership is
|
|
104
|
+
// outside the LRU so it can never be evicted by an idle timeout.
|
|
105
|
+
const warm = new Map()
|
|
106
|
+
const cacheLimit = 5
|
|
107
|
+
function releaseWarm(id) {
|
|
108
|
+
const entry = warm.get(id)
|
|
109
|
+
if (!entry) return
|
|
110
|
+
warm.delete(id)
|
|
111
|
+
window.clearTimeout(entry.timer)
|
|
112
|
+
entry.reference.release()
|
|
113
|
+
}
|
|
114
|
+
function clearWarm() {
|
|
115
|
+
for (const id of warm.keys()) releaseWarm(id)
|
|
116
|
+
}
|
|
117
|
+
function trimWarm(limit) {
|
|
118
|
+
while (warm.size > limit) releaseWarm(warm.keys().next().value)
|
|
119
|
+
}
|
|
120
|
+
function stop(force = true, keepWarm = false) {
|
|
121
|
+
if (!force) return
|
|
122
|
+
if (!keepWarm) clearWarm()
|
|
60
123
|
const old = active
|
|
61
124
|
active = undefined
|
|
62
125
|
if (!old) return
|
|
@@ -65,7 +128,15 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
65
128
|
window.clearTimeout(old.refresh)
|
|
66
129
|
old.unsubscribe?.()
|
|
67
130
|
old.unsubscribeStatus?.()
|
|
68
|
-
old.reference
|
|
131
|
+
if (keepWarm && old.reference && old.source) {
|
|
132
|
+
releaseWarm(old.id)
|
|
133
|
+
const entry = { reference: old.reference, source: old.source, contentCache: old.contentCache }
|
|
134
|
+
entry.timer = window.setTimeout(() => {
|
|
135
|
+
if (warm.get(old.id) === entry) releaseWarm(old.id)
|
|
136
|
+
}, 30000)
|
|
137
|
+
warm.set(old.id, entry)
|
|
138
|
+
trimWarm(cacheLimit)
|
|
139
|
+
} else old.reference?.release()
|
|
69
140
|
old.panel.hidePopover?.()
|
|
70
141
|
old.panel.hidden = true
|
|
71
142
|
old.panel.removeAttribute('data-prts-preview-editing')
|
|
@@ -80,19 +151,25 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
80
151
|
old.panel.remove()
|
|
81
152
|
if (old.originalParent?.isConnected) old.originalParent.append(old.panel)
|
|
82
153
|
}
|
|
154
|
+
onPreviewClosed?.()
|
|
83
155
|
}
|
|
84
156
|
function show(id, panel, title, anchor) {
|
|
85
|
-
if (active?.sticky && active.id !== id) return
|
|
86
157
|
if (active?.id === id) {
|
|
87
158
|
window.clearTimeout(active.leaveTimer)
|
|
88
159
|
return
|
|
89
160
|
}
|
|
90
|
-
stop()
|
|
161
|
+
stop(true, true)
|
|
162
|
+
const cached = warm.get(id)
|
|
163
|
+
if (cached) { window.clearTimeout(cached.timer); warm.delete(id) }
|
|
164
|
+
// Reserve one slot for the active preview, including a pending acquisition.
|
|
165
|
+
trimWarm(cacheLimit - 1)
|
|
91
166
|
const current = { id, panel, anchor, originalParent: panel.parentElement,
|
|
92
167
|
group: anchor?.closest('[data-prts-session-summary-group]') }
|
|
93
168
|
current.group?.setAttribute('data-prts-preview-open', '')
|
|
94
169
|
anchor?.setAttribute('data-prts-preview-active', '')
|
|
95
170
|
active = current
|
|
171
|
+
current.contentCache = cached?.contentCache ?? createPreviewContentCache(panel.ownerDocument)
|
|
172
|
+
if (cached) { current.reference = cached.reference; current.source = cached.source }
|
|
96
173
|
const keep = () => window.clearTimeout(current.leaveTimer)
|
|
97
174
|
const onDoubleClick = event => {
|
|
98
175
|
if (active !== current || event.button !== 0 || getPreviewPreferences?.().previewDoubleClick === false) return
|
|
@@ -135,6 +212,14 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
135
212
|
current.detached = true
|
|
136
213
|
panel.showPopover?.()
|
|
137
214
|
const label = panel.querySelector('strong')
|
|
215
|
+
let sessionTitle = panel.querySelector('[data-prts-preview-session-title]')
|
|
216
|
+
if (!sessionTitle) {
|
|
217
|
+
sessionTitle = panel.ownerDocument.createElement('div')
|
|
218
|
+
sessionTitle.setAttribute('data-prts-preview-session-title', '')
|
|
219
|
+
panel.prepend(sessionTitle)
|
|
220
|
+
}
|
|
221
|
+
sessionTitle.textContent = title
|
|
222
|
+
sessionTitle.title = title
|
|
138
223
|
let body = panel.querySelector('[data-prts-preview-body]')
|
|
139
224
|
if (!body) {
|
|
140
225
|
body = panel.ownerDocument.createElement('div')
|
|
@@ -191,7 +276,10 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
191
276
|
await pending.answer(approval
|
|
192
277
|
? selected.has('仅允许本次') ? 'allowed-once' : 'rejected'
|
|
193
278
|
: {answers:[{id:question.id,selected:question.options.map(o=>o.label).filter(value=>selected.has(value))}]})
|
|
194
|
-
if (active === current) {
|
|
279
|
+
if (active === current && !follows('warning')) {
|
|
280
|
+
stop(true, true)
|
|
281
|
+
onAnswerSubmitted?.('warning')
|
|
282
|
+
} else if (active === current) {
|
|
195
283
|
current.sticky = true
|
|
196
284
|
keep()
|
|
197
285
|
current.answeredPending = pending
|
|
@@ -259,10 +347,12 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
259
347
|
}
|
|
260
348
|
}
|
|
261
349
|
let lastChoices = ''
|
|
262
|
-
let source
|
|
350
|
+
let source = cached?.source
|
|
263
351
|
let previousText
|
|
264
352
|
let previousQuestion
|
|
265
353
|
let historyKeys = []
|
|
354
|
+
const selectReplies = createPreviewReplySelector()
|
|
355
|
+
const replyLimit = () => getPreviewPreferences?.().previewReplyLimit ?? 5
|
|
266
356
|
let detailKey
|
|
267
357
|
let detailEntry
|
|
268
358
|
const hideDetail = () => {
|
|
@@ -293,9 +383,9 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
293
383
|
detailEntry?.removeAttribute('data-prts-reply-active')
|
|
294
384
|
detailEntry = item
|
|
295
385
|
item?.setAttribute('data-prts-reply-active', '')
|
|
296
|
-
detail.querySelector('strong').textContent = '回复 ' + (index + 1)
|
|
386
|
+
detail.querySelector('strong').textContent = '回复 ' + (reply.ordinal ?? index + 1)
|
|
297
387
|
const content = detail.querySelector('[data-prts-preview-body]')
|
|
298
|
-
content.innerHTML =
|
|
388
|
+
content.innerHTML = current.contentCache(reply.text).html
|
|
299
389
|
content.scrollTop = 0
|
|
300
390
|
detail.hidden = false
|
|
301
391
|
const rect = panel.getBoundingClientRect()
|
|
@@ -316,6 +406,7 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
316
406
|
const question = approval || pending?.kind === 'question' || pending?.kind === 'plan-review'
|
|
317
407
|
const state = question ? 'warning' : status?.running || (current.answeredPending && status?.pendingInteraction === current.answeredPending) ? 'ongoing'
|
|
318
408
|
: status?.completionUnread || status?.running === false ? 'done' : current.group?.getAttribute('data-prts-session-summary-group') || ''
|
|
409
|
+
if (current.state && current.state !== state && !follows(current.state)) return
|
|
319
410
|
if (current.state && current.state !== state) {
|
|
320
411
|
current.sticky = true
|
|
321
412
|
keep()
|
|
@@ -346,39 +437,53 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
346
437
|
choices.hidden = !labels.length
|
|
347
438
|
panel.setAttribute('aria-label', title + ':' + label.textContent)
|
|
348
439
|
if (state === 'ongoing' && source) {
|
|
349
|
-
const replies =
|
|
440
|
+
const replies = selectReplies(source.getSnapshot(), replyLimit())
|
|
441
|
+
if (replies === historyKeys) return
|
|
350
442
|
panel.toggleAttribute('data-prts-preview-empty', !replies.length)
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
443
|
+
const previousTop = body.scrollTop
|
|
444
|
+
const scrollAnchor = previousTop > 0 ? [...body.children].find(entry => entry.getBoundingClientRect().bottom > body.getBoundingClientRect().top) : undefined
|
|
445
|
+
const anchorTop = scrollAnchor?.getBoundingClientRect().top
|
|
446
|
+
const retained = new Map(historyKeys.map(reply => [reply.key, reply]))
|
|
447
|
+
for (const old of historyKeys) {
|
|
448
|
+
const replacement = replies.find(reply => reply.key === old.key)
|
|
449
|
+
if (!replacement || replacement.text !== old.text || replacement.ordinal !== old.ordinal) {
|
|
450
|
+
body.querySelectorAll('[data-prts-preview-message]').forEach(entry => {
|
|
451
|
+
if (entry.dataset.replyKey === old.key) entry.remove()
|
|
452
|
+
})
|
|
453
|
+
retained.delete(old.key)
|
|
454
|
+
if (detailKey === old.key) hideDetail()
|
|
455
|
+
}
|
|
357
456
|
}
|
|
457
|
+
if (!historyKeys.length) body.replaceChildren()
|
|
358
458
|
if (!replies.length) body.textContent = '正在处理,等待本轮正文输出完成…'
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
459
|
+
for (let index = 0; index < replies.length; index++) {
|
|
460
|
+
const reply = replies[index]
|
|
461
|
+
if (retained.has(reply.key)) continue
|
|
362
462
|
const entry = panel.ownerDocument.createElement('article')
|
|
363
463
|
entry.setAttribute('data-prts-preview-message', '')
|
|
464
|
+
entry.dataset.replyKey = reply.key
|
|
364
465
|
const item = panel.ownerDocument.createElement('button')
|
|
365
466
|
item.type = 'button'
|
|
366
467
|
item.setAttribute('data-prts-reply-entry', '')
|
|
367
468
|
const heading = panel.ownerDocument.createElement('small')
|
|
368
|
-
heading.textContent = '回复 ' +
|
|
469
|
+
heading.textContent = '回复 ' + reply.ordinal
|
|
369
470
|
const summary = panel.ownerDocument.createElement('span')
|
|
370
|
-
|
|
371
|
-
plain.innerHTML = renderPreviewMarkdown(replies[index].text)
|
|
372
|
-
summary.textContent = plain.textContent.replace(/\s+/g, ' ').trim().slice(0, 100)
|
|
471
|
+
summary.textContent = current.contentCache(reply.text).summary
|
|
373
472
|
item.append(heading, summary)
|
|
374
|
-
item.addEventListener('pointermove', () => showDetail(
|
|
375
|
-
item.addEventListener('focus', () => showDetail(
|
|
376
|
-
item.addEventListener('click', () => showDetail(
|
|
473
|
+
item.addEventListener('pointermove', () => showDetail(reply, index, item))
|
|
474
|
+
item.addEventListener('focus', () => showDetail(reply, index, item))
|
|
475
|
+
item.addEventListener('click', () => showDetail(reply, index, item))
|
|
377
476
|
entry.append(item)
|
|
378
477
|
body.prepend(entry)
|
|
379
478
|
}
|
|
380
|
-
|
|
381
|
-
|
|
479
|
+
// Reorder only when necessary, preserving retained DOM and open details.
|
|
480
|
+
const entries = new Map([...body.children].map(entry => [entry.dataset.replyKey, entry]))
|
|
481
|
+
for (let i = replies.length - 1, position = 0; i >= 0; i--, position++) {
|
|
482
|
+
const entry = entries.get(replies[i].key)
|
|
483
|
+
if (body.children[position] !== entry) body.insertBefore(entry, body.children[position] ?? null)
|
|
484
|
+
}
|
|
485
|
+
if (scrollAnchor?.isConnected) body.scrollTop += scrollAnchor.getBoundingClientRect().top - anchorTop
|
|
486
|
+
else if (previousTop === 0) body.scrollTop = 0
|
|
382
487
|
historyKeys = replies
|
|
383
488
|
previousText = undefined
|
|
384
489
|
previousQuestion = undefined
|
|
@@ -396,14 +501,20 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
396
501
|
if (previousText !== text || previousQuestion !== question) {
|
|
397
502
|
const extendsReply = previousQuestion === question && previousText && text.startsWith(previousText)
|
|
398
503
|
if (question) body.textContent = text
|
|
399
|
-
else body.innerHTML =
|
|
504
|
+
else body.innerHTML = current.contentCache(text).html
|
|
400
505
|
previousText = text
|
|
401
506
|
previousQuestion = question
|
|
402
507
|
if (!extendsReply) body.scrollTop = 0
|
|
403
508
|
}
|
|
404
509
|
if (canAnswerPreview(pending)) enableAnswer()
|
|
405
510
|
}
|
|
406
|
-
|
|
511
|
+
let statusFields = previewStatusFields(getSessionStatus?.()?.getSnapshot().get(id))
|
|
512
|
+
current.unsubscribeStatus = getSessionStatus?.()?.subscribe(() => {
|
|
513
|
+
const next = previewStatusFields(getSessionStatus?.()?.getSnapshot().get(id))
|
|
514
|
+
if (next.every((value, index) => value === statusFields[index])) return
|
|
515
|
+
statusFields = next
|
|
516
|
+
render()
|
|
517
|
+
})
|
|
407
518
|
render()
|
|
408
519
|
body.scrollTop = 0
|
|
409
520
|
current.delay = window.setTimeout(async () => {
|
|
@@ -411,14 +522,16 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
411
522
|
const service = getUiConversation?.()
|
|
412
523
|
const controller = getSessions?.() ?? sessions
|
|
413
524
|
if (!controller?.retain || !service?.binding) throw new Error('unavailable')
|
|
414
|
-
current.reference = controller.retain(id, { source: 'prtsSummaryPreview' })
|
|
525
|
+
if (!current.reference) current.reference = controller.retain(id, { source: 'prtsSummaryPreview' })
|
|
415
526
|
const binding = await current.reference.ready
|
|
416
527
|
if (active !== current) return
|
|
417
|
-
source
|
|
528
|
+
source ??= service.binding(binding).target('chat')
|
|
529
|
+
current.source = source
|
|
418
530
|
current.unsubscribe = source.subscribe(() => {
|
|
419
531
|
if (active !== current || current.refresh !== undefined) return
|
|
420
532
|
current.refresh = window.setTimeout(() => {
|
|
421
533
|
current.refresh = undefined
|
|
534
|
+
if (current.state === 'ongoing' && !current.editor && selectReplies(source.getSnapshot(), replyLimit()) === historyKeys) return
|
|
422
535
|
render()
|
|
423
536
|
}, 120)
|
|
424
537
|
})
|
|
@@ -432,14 +545,14 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
432
545
|
render()
|
|
433
546
|
if (body.textContent.trim() === '正在读取…') body.textContent = '暂时无法读取回复,可点击会话查看'
|
|
434
547
|
}
|
|
435
|
-
}, 80)
|
|
548
|
+
}, cached ? 0 : 80)
|
|
436
549
|
}
|
|
437
550
|
function leave() {
|
|
438
551
|
if (!active || active.sticky) return
|
|
439
552
|
const current = active
|
|
440
553
|
window.clearTimeout(current.leaveTimer)
|
|
441
554
|
current.leaveTimer = window.setTimeout(() => {
|
|
442
|
-
if (active === current && !current.panel.matches(':hover')) stop()
|
|
555
|
+
if (active === current && !current.panel.matches(':hover')) stop(true, true)
|
|
443
556
|
}, 300)
|
|
444
557
|
}
|
|
445
558
|
function reanchor(root) {
|
|
@@ -455,7 +568,14 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
455
568
|
anchor?.setAttribute('data-prts-preview-active', '')
|
|
456
569
|
window.clearTimeout(active.leaveTimer)
|
|
457
570
|
}
|
|
458
|
-
|
|
571
|
+
function enterState(state) {
|
|
572
|
+
if (active && active.state !== state) stop(true, true)
|
|
573
|
+
}
|
|
574
|
+
function pinnedState() {
|
|
575
|
+
return active && !follows(active.state)
|
|
576
|
+
? { id: active.id, state: active.state } : undefined
|
|
577
|
+
}
|
|
578
|
+
return { show, stop, leave, reanchor, enterState, pinnedState, isOpen: () => Boolean(active) }
|
|
459
579
|
}
|
|
460
580
|
|
|
461
581
|
export function scrollSummaryTitle(button, viewport, text, window) {
|
|
@@ -203,9 +203,21 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
203
203
|
let statusSourcesStarted = false
|
|
204
204
|
let summaryRoot
|
|
205
205
|
let summarySignature = ''
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
let heldPreviewState
|
|
207
|
+
function dismissHeldPreview(event) {
|
|
208
|
+
if (!heldPreviewState || (event?.type === 'keydown' && event.key !== 'Escape')) return
|
|
209
|
+
if (event?.type === 'pointerdown' && (summaryRoot?.contains(event.target) || event.target.closest?.('[data-prts-summary-reply]'))) return
|
|
210
|
+
heldPreviewState = undefined
|
|
211
|
+
queueScan()
|
|
212
|
+
}
|
|
213
|
+
function enterPreviewState(state) {
|
|
214
|
+
if (heldPreviewState && heldPreviewState !== state) { heldPreviewState = undefined; queueScan() }
|
|
215
|
+
preview.enterState(state)
|
|
216
|
+
}
|
|
217
|
+
const preview = createSessionSummaryPreview({ document, window, sessions, getUiConversation, getSessions, getSessionStatus, openSession, getPreviewPreferences: () => previewPreferences, onPreviewClosed: () => { if (statusSourcesStarted) queueScan() }, onAnswerSubmitted: state => { heldPreviewState = state; queueScan() } })
|
|
218
|
+
const stopTitles = new Map()
|
|
219
|
+
const summaryGroups = new Map()
|
|
220
|
+
function clearSummaryEffects() { preview.stop(false); for (const stop of stopTitles.values()) stop(); stopTitles.clear(); summaryGroups.clear() }
|
|
209
221
|
let owned = new Set()
|
|
210
222
|
let vectorOwners = new Set()
|
|
211
223
|
const dirtyVectors = new Map()
|
|
@@ -221,6 +233,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
221
233
|
}
|
|
222
234
|
|
|
223
235
|
function openSession(sessionId) {
|
|
236
|
+
dismissHeldPreview()
|
|
224
237
|
try {
|
|
225
238
|
const uiWorkspace = getUiWorkspace?.()
|
|
226
239
|
if (typeof uiWorkspace?.openSession === 'function') {
|
|
@@ -261,6 +274,8 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
261
274
|
count.setAttribute('data-prts-session-summary-count', '')
|
|
262
275
|
count.textContent = String(items.length)
|
|
263
276
|
trigger.append(count)
|
|
277
|
+
trigger.addEventListener('pointerenter', () => enterPreviewState(state))
|
|
278
|
+
trigger.addEventListener('focus', () => enterPreviewState(state))
|
|
264
279
|
|
|
265
280
|
const popover = document.createElement('span')
|
|
266
281
|
popover.setAttribute('data-prts-session-summary-popover', state)
|
|
@@ -287,7 +302,35 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
287
302
|
const list = document.createElement('span')
|
|
288
303
|
list.setAttribute('data-prts-summary-list', '')
|
|
289
304
|
popover.append(list)
|
|
290
|
-
|
|
305
|
+
const entries = new Map()
|
|
306
|
+
const update = items => {
|
|
307
|
+
count.textContent = String(items.length)
|
|
308
|
+
trigger.setAttribute('aria-label', `${label} ${items.length}`)
|
|
309
|
+
const ids = new Set(items.map(item => item.id))
|
|
310
|
+
for (const [id, entry] of entries) {
|
|
311
|
+
if (ids.has(id)) continue
|
|
312
|
+
stopTitles.get(entry.button)?.()
|
|
313
|
+
stopTitles.delete(entry.button)
|
|
314
|
+
entry.button.remove()
|
|
315
|
+
entries.delete(id)
|
|
316
|
+
}
|
|
317
|
+
for (const [index, incoming] of items.entries()) {
|
|
318
|
+
const existing = entries.get(incoming.id)
|
|
319
|
+
if (existing) {
|
|
320
|
+
if (existing.item.title !== incoming.title) {
|
|
321
|
+
stopTitles.get(existing.button)?.()
|
|
322
|
+
existing.button.querySelector('[data-prts-summary-title] > span').textContent = incoming.title
|
|
323
|
+
}
|
|
324
|
+
Object.assign(existing.item, incoming)
|
|
325
|
+
let detail = existing.button.querySelector('[data-prts-session-summary-item-status]')
|
|
326
|
+
if (incoming.statusLabel && !detail) { detail = document.createElement('small'); detail.setAttribute('data-prts-session-summary-item-status',''); existing.button.append(detail) }
|
|
327
|
+
if (incoming.statusLabel) detail.textContent = incoming.statusLabel
|
|
328
|
+
else detail?.remove()
|
|
329
|
+
existing.button.setAttribute('aria-label', incoming.statusLabel ? `${incoming.title},${incoming.statusLabel}` : incoming.title)
|
|
330
|
+
if (list.children[index] !== existing.button) list.insertBefore(existing.button, list.children[index] ?? null)
|
|
331
|
+
continue
|
|
332
|
+
}
|
|
333
|
+
const item = {...incoming}
|
|
291
334
|
const button = document.createElement('button')
|
|
292
335
|
button.type = 'button'
|
|
293
336
|
button.setAttribute('role', 'menuitem')
|
|
@@ -299,14 +342,18 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
299
342
|
text.textContent = item.title
|
|
300
343
|
title.append(text)
|
|
301
344
|
button.append(title)
|
|
345
|
+
const viewing = document.createElement('em')
|
|
346
|
+
viewing.setAttribute('data-prts-preview-marker', '')
|
|
347
|
+
viewing.textContent = '预览中'
|
|
348
|
+
button.append(viewing)
|
|
302
349
|
button.setAttribute('aria-label', item.title)
|
|
303
|
-
stopTitles.
|
|
350
|
+
stopTitles.set(button, scrollSummaryTitle(button, title, text, window))
|
|
304
351
|
const showReply = () => {
|
|
305
352
|
const key = {ongoing:'previewOngoing',warning:'previewWarning',done:'previewDone'}[state]
|
|
306
353
|
if (previewPreferences[key] === false) { preview.stop(); return }
|
|
307
354
|
preview.show(item.id, reply, item.title, button)
|
|
308
355
|
}
|
|
309
|
-
button.addEventListener('
|
|
356
|
+
button.addEventListener('pointermove', showReply)
|
|
310
357
|
button.addEventListener('pointerleave', () => preview.leave())
|
|
311
358
|
button.addEventListener('focus', showReply)
|
|
312
359
|
if (item.statusLabel) {
|
|
@@ -321,8 +368,15 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
321
368
|
preview.stop()
|
|
322
369
|
openSession(item.id)
|
|
323
370
|
})
|
|
324
|
-
list.
|
|
371
|
+
list.insertBefore(button, list.children[index] ?? null)
|
|
372
|
+
entries.set(item.id, {item, button})
|
|
373
|
+
}
|
|
325
374
|
}
|
|
375
|
+
update(items)
|
|
376
|
+
summaryGroups.set(state, {group, update, dispose: () => {
|
|
377
|
+
for (const {button} of entries.values()) { stopTitles.get(button)?.(); stopTitles.delete(button) }
|
|
378
|
+
group.remove()
|
|
379
|
+
}})
|
|
326
380
|
popover.append(reply)
|
|
327
381
|
group.append(trigger, popover)
|
|
328
382
|
return group
|
|
@@ -331,6 +385,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
331
385
|
function syncSummary(root, next) {
|
|
332
386
|
const { header, label } = resolveSectionHeader(root)
|
|
333
387
|
if (previewPreferences.sessionPreview === false || !header || !label) {
|
|
388
|
+
heldPreviewState = undefined
|
|
334
389
|
preview.stop()
|
|
335
390
|
clearSummaryEffects()
|
|
336
391
|
summaryRoot?.remove()
|
|
@@ -361,10 +416,31 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
361
416
|
rows[index] = { ...rows[index], state: nativeRow.state }
|
|
362
417
|
}
|
|
363
418
|
}
|
|
364
|
-
|
|
365
|
-
const
|
|
366
|
-
|
|
419
|
+
// Do not move the row/menu to another status behind a frozen preview.
|
|
420
|
+
const pinned = preview.pinnedState()
|
|
421
|
+
if (pinned?.state) {
|
|
422
|
+
const item = rows.find(item => item.id === pinned.id)
|
|
423
|
+
if (item) { item.state = pinned.state; item.statusLabel = undefined }
|
|
424
|
+
}
|
|
425
|
+
// While reading a preview, preserve surviving rows' positions and append
|
|
426
|
+
// newcomers. No persistent ordering: the next idle sync uses host order.
|
|
427
|
+
const orderedState = state => {
|
|
428
|
+
const items = rows.filter(item => item.state === state)
|
|
429
|
+
if (!preview.isOpen() || !summaryRoot) return items
|
|
430
|
+
const remaining = new Map(items.map(item => [item.id, item]))
|
|
431
|
+
const stable = []
|
|
432
|
+
for (const row of summaryRoot.querySelectorAll(`[data-prts-session-summary-group="${state}"] [data-prts-session-summary-item]`)) {
|
|
433
|
+
const id = row.getAttribute('data-prts-session-summary-item')
|
|
434
|
+
if (remaining.has(id)) { stable.push(remaining.get(id)); remaining.delete(id) }
|
|
435
|
+
}
|
|
436
|
+
return [...stable, ...remaining.values()]
|
|
437
|
+
}
|
|
438
|
+
const warning = orderedState('warning')
|
|
439
|
+
const ongoing = orderedState('ongoing')
|
|
440
|
+
const done = orderedState('done')
|
|
441
|
+
if (heldPreviewState && !rows.some(item => item.state === heldPreviewState)) heldPreviewState = undefined
|
|
367
442
|
const signature = JSON.stringify({
|
|
443
|
+
heldPreviewState,
|
|
368
444
|
warning: warning.map(item => [item.id, item.title, item.statusLabel]),
|
|
369
445
|
ongoing: ongoing.map(item => [item.id, item.title]),
|
|
370
446
|
done: done.map(item => [item.id, item.title]),
|
|
@@ -384,12 +460,20 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
384
460
|
summaryRoot.hidden = warning.length === 0 && ongoing.length === 0 && done.length === 0
|
|
385
461
|
if (signature === summarySignature) return
|
|
386
462
|
summarySignature = signature
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
463
|
+
for (const [state, title, items] of [['warning','等待处理的会话',warning],['ongoing','运行中的会话',ongoing],['done','完成未查看',done]]) {
|
|
464
|
+
const existing = summaryGroups.get(state)
|
|
465
|
+
if (!items.length) { existing?.dispose(); summaryGroups.delete(state); continue }
|
|
466
|
+
if (existing) existing.update(items)
|
|
467
|
+
else summaryRoot.append(buildSummaryGroup(state, title, items))
|
|
468
|
+
}
|
|
469
|
+
let position = 0
|
|
470
|
+
for (const state of ['warning','ongoing','done']) {
|
|
471
|
+
const group = summaryGroups.get(state)?.group
|
|
472
|
+
if (group && summaryRoot.children[position++] !== group) summaryRoot.insertBefore(group, summaryRoot.children[position - 1] ?? null)
|
|
473
|
+
group?.removeAttribute('data-prts-preview-open')
|
|
474
|
+
}
|
|
392
475
|
preview.reanchor(summaryRoot)
|
|
476
|
+
if (heldPreviewState) summaryRoot.querySelector(`[data-prts-session-summary-group="${heldPreviewState}"]`)?.setAttribute('data-prts-preview-open', '')
|
|
393
477
|
}
|
|
394
478
|
|
|
395
479
|
function scheduleVectors() {
|
|
@@ -567,7 +651,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
567
651
|
|
|
568
652
|
return {
|
|
569
653
|
start(preferences = {}) {
|
|
570
|
-
const keys = ['sessionPreview','previewOngoing','previewWarning','previewDone','previewDoubleClick']
|
|
654
|
+
const keys = ['sessionPreview','previewOngoing','previewWarning','previewDone','previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning', 'previewReplyLimit']
|
|
571
655
|
if (keys.some(key => previewPreferences[key] !== preferences[key])) preview.stop()
|
|
572
656
|
previewPreferences = Object.fromEntries(keys.map(key => [key, preferences[key]]))
|
|
573
657
|
if (!document) return
|
|
@@ -585,6 +669,8 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
585
669
|
flushVectors()
|
|
586
670
|
})
|
|
587
671
|
}
|
|
672
|
+
document.addEventListener('pointerdown', dismissHeldPreview)
|
|
673
|
+
document.addEventListener('keydown', dismissHeldPreview)
|
|
588
674
|
scan()
|
|
589
675
|
if (!statusSourcesStarted) {
|
|
590
676
|
statusSourcesStarted = true
|
|
@@ -605,6 +691,9 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
605
691
|
})
|
|
606
692
|
},
|
|
607
693
|
dispose() {
|
|
694
|
+
heldPreviewState = undefined
|
|
695
|
+
document?.removeEventListener('pointerdown', dismissHeldPreview)
|
|
696
|
+
document?.removeEventListener('keydown', dismissHeldPreview)
|
|
608
697
|
observer?.disconnect()
|
|
609
698
|
observer = undefined
|
|
610
699
|
stopSessionList()
|