@ash-qw/dsh-theme-prts 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/lib/client.js +759 -129
- package/package.json +1 -1
- package/src/client/index.js +1 -2
- package/src/client/preferences.js +7 -1
- package/src/client/preview-markdown.js +26 -0
- package/src/client/session-lifeline.js +1 -1
- package/src/client/session-summary-preview.js +273 -39
- package/src/client/sidebar-control-adapter.js +465 -54
- package/src/client/theme-settings-workbench.js +29 -48
- package/src/styles/facility-vector.css +110 -21
- package/src/styles/prts.css +5 -5
- package/src/styles/refinement.css +1 -1
- package/src/styles/theme-settings-workbench.css +13 -32
package/package.json
CHANGED
package/src/client/index.js
CHANGED
|
@@ -96,11 +96,10 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
96
96
|
const sidebarControls = createSidebarControlAdapter({
|
|
97
97
|
document,
|
|
98
98
|
window,
|
|
99
|
-
sessions: contextService(ctx, 'sessions'),
|
|
100
|
-
workspaces: contextService(ctx, 'workspaces'),
|
|
101
99
|
getSessionStatus: () => contextService(ctx, 'uiSession')?.sessionStatus,
|
|
102
100
|
getUiWorkspace: () => contextService(ctx, 'uiWorkspace'),
|
|
103
101
|
getSessions: () => contextService(ctx, 'sessions'),
|
|
102
|
+
getWorkspaces: () => contextService(ctx, 'workspaces'),
|
|
104
103
|
getUiConversation: () => contextService(ctx, 'uiConversation'),
|
|
105
104
|
})
|
|
106
105
|
const floatingGlass = createFloatingGlassAdapter({ document, window })
|
|
@@ -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
|
+
}
|
|
@@ -138,7 +138,7 @@ export function createSessionLifelineAnimator({ document, window }) {
|
|
|
138
138
|
return Boolean(
|
|
139
139
|
row?.isConnected
|
|
140
140
|
&& lifeline?.isConnected
|
|
141
|
-
&& row.getAttribute('aria-selected') === 'true'
|
|
141
|
+
&& (row.hasAttribute('data-prts-selection-target') ? row.getAttribute('data-prts-selection-target') === 'true' : row.getAttribute('aria-selected') === 'true')
|
|
142
142
|
&& document?.visibilityState !== 'hidden'
|
|
143
143
|
&& document?.documentElement?.dataset?.prtsSessionFlow !== 'off'
|
|
144
144
|
&& document?.documentElement?.dataset?.prtsMotion !== 'reduced'
|
|
@@ -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,89 @@ 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?.callId, pending?.toolName, pending?.reason, pending?.displayReason])]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function approvalCallDetails(snapshot, pending) {
|
|
98
|
+
if (!pending?.callId) return undefined
|
|
99
|
+
for (const node of snapshot?.nodes?.values?.() ?? []) {
|
|
100
|
+
const root = node.kind === 'tool-call' ? node.data?.root : undefined
|
|
101
|
+
if (!root || root.callId !== pending.callId || 'kind' in root || root.phase !== 'start') continue
|
|
102
|
+
const raw = root.argsRaw
|
|
103
|
+
if (typeof raw !== 'string') return undefined
|
|
104
|
+
try {
|
|
105
|
+
const args = JSON.parse(raw)
|
|
106
|
+
if (args && typeof args === 'object' && !Array.isArray(args) && typeof args.command === 'string') {
|
|
107
|
+
const {command, ...rest} = args
|
|
108
|
+
return {command, parameters: Object.keys(rest).length ? JSON.stringify(rest, null, 2) : ''}
|
|
109
|
+
}
|
|
110
|
+
return {parameters: JSON.stringify(args, null, 2)}
|
|
111
|
+
} catch { return {parameters: raw} }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function createSessionSummaryPreview({ document, window, sessions, getSessions, getUiConversation, getSessionStatus, openSession, getPreviewPreferences, onPreviewClosed, onAnswerSubmitted }) {
|
|
57
116
|
let active
|
|
58
|
-
function
|
|
59
|
-
|
|
117
|
+
function follows(state) {
|
|
118
|
+
const preferences = getPreviewPreferences?.() ?? {}
|
|
119
|
+
return state === 'warning' ? preferences.previewFollowWarning === true : preferences.previewFollowOngoing !== false
|
|
120
|
+
}
|
|
121
|
+
// Shared across running, pending and completed previews. Active ownership is
|
|
122
|
+
// outside the LRU so it can never be evicted by an idle timeout.
|
|
123
|
+
const warm = new Map()
|
|
124
|
+
const cacheLimit = 5
|
|
125
|
+
function releaseWarm(id) {
|
|
126
|
+
const entry = warm.get(id)
|
|
127
|
+
if (!entry) return
|
|
128
|
+
warm.delete(id)
|
|
129
|
+
window.clearTimeout(entry.timer)
|
|
130
|
+
entry.reference.release()
|
|
131
|
+
}
|
|
132
|
+
function clearWarm() {
|
|
133
|
+
for (const id of warm.keys()) releaseWarm(id)
|
|
134
|
+
}
|
|
135
|
+
function trimWarm(limit) {
|
|
136
|
+
while (warm.size > limit) releaseWarm(warm.keys().next().value)
|
|
137
|
+
}
|
|
138
|
+
function stop(force = true, keepWarm = false) {
|
|
139
|
+
if (!force) return
|
|
140
|
+
if (!keepWarm) clearWarm()
|
|
60
141
|
const old = active
|
|
61
142
|
active = undefined
|
|
62
143
|
if (!old) return
|
|
@@ -65,7 +146,15 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
65
146
|
window.clearTimeout(old.refresh)
|
|
66
147
|
old.unsubscribe?.()
|
|
67
148
|
old.unsubscribeStatus?.()
|
|
68
|
-
old.reference
|
|
149
|
+
if (keepWarm && old.reference && old.source) {
|
|
150
|
+
releaseWarm(old.id)
|
|
151
|
+
const entry = { reference: old.reference, source: old.source, contentCache: old.contentCache }
|
|
152
|
+
entry.timer = window.setTimeout(() => {
|
|
153
|
+
if (warm.get(old.id) === entry) releaseWarm(old.id)
|
|
154
|
+
}, 30000)
|
|
155
|
+
warm.set(old.id, entry)
|
|
156
|
+
trimWarm(cacheLimit)
|
|
157
|
+
} else old.reference?.release()
|
|
69
158
|
old.panel.hidePopover?.()
|
|
70
159
|
old.panel.hidden = true
|
|
71
160
|
old.panel.removeAttribute('data-prts-preview-editing')
|
|
@@ -80,19 +169,25 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
80
169
|
old.panel.remove()
|
|
81
170
|
if (old.originalParent?.isConnected) old.originalParent.append(old.panel)
|
|
82
171
|
}
|
|
172
|
+
onPreviewClosed?.()
|
|
83
173
|
}
|
|
84
174
|
function show(id, panel, title, anchor) {
|
|
85
|
-
if (active?.sticky && active.id !== id) return
|
|
86
175
|
if (active?.id === id) {
|
|
87
176
|
window.clearTimeout(active.leaveTimer)
|
|
88
177
|
return
|
|
89
178
|
}
|
|
90
|
-
stop()
|
|
179
|
+
stop(true, true)
|
|
180
|
+
const cached = warm.get(id)
|
|
181
|
+
if (cached) { window.clearTimeout(cached.timer); warm.delete(id) }
|
|
182
|
+
// Reserve one slot for the active preview, including a pending acquisition.
|
|
183
|
+
trimWarm(cacheLimit - 1)
|
|
91
184
|
const current = { id, panel, anchor, originalParent: panel.parentElement,
|
|
92
185
|
group: anchor?.closest('[data-prts-session-summary-group]') }
|
|
93
186
|
current.group?.setAttribute('data-prts-preview-open', '')
|
|
94
187
|
anchor?.setAttribute('data-prts-preview-active', '')
|
|
95
188
|
active = current
|
|
189
|
+
current.contentCache = cached?.contentCache ?? createPreviewContentCache(panel.ownerDocument)
|
|
190
|
+
if (cached) { current.reference = cached.reference; current.source = cached.source }
|
|
96
191
|
const keep = () => window.clearTimeout(current.leaveTimer)
|
|
97
192
|
const onDoubleClick = event => {
|
|
98
193
|
if (active !== current || event.button !== 0 || getPreviewPreferences?.().previewDoubleClick === false) return
|
|
@@ -134,7 +229,82 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
134
229
|
panel.ownerDocument.body.append(panel)
|
|
135
230
|
current.detached = true
|
|
136
231
|
panel.showPopover?.()
|
|
232
|
+
let positionFrame
|
|
233
|
+
const positionPanels = () => {
|
|
234
|
+
positionFrame = undefined
|
|
235
|
+
if (active !== current) return
|
|
236
|
+
const viewport = window.visualViewport
|
|
237
|
+
const left = (viewport?.offsetLeft ?? 0) + 8, top = (viewport?.offsetTop ?? 0) + 8
|
|
238
|
+
const width = Math.max(1, (viewport?.width ?? window.innerWidth) - 16)
|
|
239
|
+
const height = Math.max(1, (viewport?.height ?? window.innerHeight) - 16)
|
|
240
|
+
const place = (surface, reference, preferredTop, noticeOwner, anchorBottom = preferredTop) => {
|
|
241
|
+
surface.style.maxWidth = 'none'
|
|
242
|
+
surface.style.maxHeight = 'none'
|
|
243
|
+
const bounds = surface.getBoundingClientRect()
|
|
244
|
+
const x = reference ? reference.right + 10 : left
|
|
245
|
+
const fitsVertically = y => y >= top && y + bounds.height <= top + height
|
|
246
|
+
const above = anchorBottom - bounds.height
|
|
247
|
+
const y = !fitsVertically(preferredTop) && fitsVertically(above) ? above : preferredTop
|
|
248
|
+
const insufficient = Boolean(reference) && (x < left || x + bounds.width > left + width || !fitsVertically(y))
|
|
249
|
+
surface.toggleAttribute('data-prts-preview-no-space', insufficient)
|
|
250
|
+
const noticeKey = surface === panel ? 'spaceNotice' : 'detailSpaceNotice'
|
|
251
|
+
if (insufficient && noticeOwner) {
|
|
252
|
+
let notice = current[noticeKey]
|
|
253
|
+
if (!notice) {
|
|
254
|
+
notice = panel.ownerDocument.createElement('div')
|
|
255
|
+
notice.setAttribute('data-prts-preview-space-notice', '')
|
|
256
|
+
notice.setAttribute('role', 'status')
|
|
257
|
+
const guidance = '空间不足,无法展开预览。可点击会话进入;建议扩大窗口,或在辅助功能中关闭对应预览。'
|
|
258
|
+
notice.textContent = surface === panel ? '空间不足,无法展开正文预览' : guidance
|
|
259
|
+
notice.title = guidance
|
|
260
|
+
current[noticeKey] = notice
|
|
261
|
+
}
|
|
262
|
+
if (notice.parentElement !== noticeOwner) noticeOwner.append(notice)
|
|
263
|
+
} else { current[noticeKey]?.remove(); current[noticeKey] = undefined }
|
|
264
|
+
surface.style.left = x + 'px'
|
|
265
|
+
surface.style.top = y + 'px'
|
|
266
|
+
return !insufficient
|
|
267
|
+
}
|
|
268
|
+
const menu = current.anchor?.closest('[data-prts-session-summary-popover]')
|
|
269
|
+
const reference = menu?.getBoundingClientRect()
|
|
270
|
+
const row = current.anchor?.getBoundingClientRect()
|
|
271
|
+
const visible = place(panel, reference, row?.top ?? (parseFloat(panel.style.top) || top), menu, row?.bottom)
|
|
272
|
+
if (current.detail && !current.detail.hidden) {
|
|
273
|
+
if (visible) place(current.detail, panel.getBoundingClientRect(), panel.getBoundingClientRect().top, panel, panel.getBoundingClientRect().bottom)
|
|
274
|
+
else current.detail.setAttribute('data-prts-preview-no-space', '')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
}
|
|
278
|
+
const schedulePosition = () => {
|
|
279
|
+
if (positionFrame === undefined) positionFrame = window.requestAnimationFrame(positionPanels)
|
|
280
|
+
}
|
|
281
|
+
const positionObserver = window.ResizeObserver ? new window.ResizeObserver(schedulePosition) : undefined
|
|
282
|
+
positionObserver?.observe(panel)
|
|
283
|
+
window.addEventListener('resize', schedulePosition)
|
|
284
|
+
window.visualViewport?.addEventListener('resize', schedulePosition)
|
|
285
|
+
window.visualViewport?.addEventListener('scroll', schedulePosition)
|
|
286
|
+
const cleanupHover = current.cleanupHover
|
|
287
|
+
current.cleanupHover = () => {
|
|
288
|
+
cleanupHover()
|
|
289
|
+
current.spaceNotice?.remove()
|
|
290
|
+
current.detailSpaceNotice?.remove()
|
|
291
|
+
panel.removeAttribute('data-prts-preview-no-space')
|
|
292
|
+
positionObserver?.disconnect()
|
|
293
|
+
window.cancelAnimationFrame(positionFrame)
|
|
294
|
+
window.removeEventListener('resize', schedulePosition)
|
|
295
|
+
window.visualViewport?.removeEventListener('resize', schedulePosition)
|
|
296
|
+
window.visualViewport?.removeEventListener('scroll', schedulePosition)
|
|
297
|
+
}
|
|
298
|
+
schedulePosition()
|
|
137
299
|
const label = panel.querySelector('strong')
|
|
300
|
+
let sessionTitle = panel.querySelector('[data-prts-preview-session-title]')
|
|
301
|
+
if (!sessionTitle) {
|
|
302
|
+
sessionTitle = panel.ownerDocument.createElement('div')
|
|
303
|
+
sessionTitle.setAttribute('data-prts-preview-session-title', '')
|
|
304
|
+
panel.prepend(sessionTitle)
|
|
305
|
+
}
|
|
306
|
+
sessionTitle.textContent = title
|
|
307
|
+
sessionTitle.title = title
|
|
138
308
|
let body = panel.querySelector('[data-prts-preview-body]')
|
|
139
309
|
if (!body) {
|
|
140
310
|
body = panel.ownerDocument.createElement('div')
|
|
@@ -163,6 +333,7 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
163
333
|
const pending = pendingNow()
|
|
164
334
|
if (!canAnswerPreview(pending)) return
|
|
165
335
|
current.editor = true
|
|
336
|
+
current.editingPending = pending
|
|
166
337
|
// Detach from the hover menu before promoting again to the top layer.
|
|
167
338
|
panel.setAttribute('data-prts-preview-editing', '')
|
|
168
339
|
panel.setAttribute('role', 'dialog')
|
|
@@ -191,7 +362,10 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
191
362
|
await pending.answer(approval
|
|
192
363
|
? selected.has('仅允许本次') ? 'allowed-once' : 'rejected'
|
|
193
364
|
: {answers:[{id:question.id,selected:question.options.map(o=>o.label).filter(value=>selected.has(value))}]})
|
|
194
|
-
if (active === current) {
|
|
365
|
+
if (active === current && !follows('warning')) {
|
|
366
|
+
stop(true, true)
|
|
367
|
+
onAnswerSubmitted?.('warning')
|
|
368
|
+
} else if (active === current) {
|
|
195
369
|
current.sticky = true
|
|
196
370
|
keep()
|
|
197
371
|
current.answeredPending = pending
|
|
@@ -259,15 +433,19 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
259
433
|
}
|
|
260
434
|
}
|
|
261
435
|
let lastChoices = ''
|
|
262
|
-
let source
|
|
436
|
+
let source = cached?.source
|
|
263
437
|
let previousText
|
|
264
438
|
let previousQuestion
|
|
265
439
|
let historyKeys = []
|
|
440
|
+
const selectReplies = createPreviewReplySelector()
|
|
441
|
+
const replyLimit = () => getPreviewPreferences?.().previewReplyLimit ?? 5
|
|
266
442
|
let detailKey
|
|
267
443
|
let detailEntry
|
|
268
444
|
const hideDetail = () => {
|
|
269
445
|
current.detail?.hidePopover?.()
|
|
270
446
|
if (current.detail) current.detail.hidden = true
|
|
447
|
+
current.detailSpaceNotice?.remove()
|
|
448
|
+
current.detailSpaceNotice = undefined
|
|
271
449
|
detailKey = undefined
|
|
272
450
|
detailEntry?.removeAttribute('data-prts-reply-active')
|
|
273
451
|
detailEntry = undefined
|
|
@@ -287,15 +465,16 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
287
465
|
detail.append(heading, content)
|
|
288
466
|
panel.append(detail)
|
|
289
467
|
current.detail = detail
|
|
468
|
+
positionObserver?.observe(detail)
|
|
290
469
|
}
|
|
291
470
|
const detail = current.detail
|
|
292
471
|
detailKey = reply.key
|
|
293
472
|
detailEntry?.removeAttribute('data-prts-reply-active')
|
|
294
473
|
detailEntry = item
|
|
295
474
|
item?.setAttribute('data-prts-reply-active', '')
|
|
296
|
-
detail.querySelector('strong').textContent = '回复 ' + (index + 1)
|
|
475
|
+
detail.querySelector('strong').textContent = '回复 ' + (reply.ordinal ?? index + 1)
|
|
297
476
|
const content = detail.querySelector('[data-prts-preview-body]')
|
|
298
|
-
content.innerHTML =
|
|
477
|
+
content.innerHTML = current.contentCache(reply.text).html
|
|
299
478
|
content.scrollTop = 0
|
|
300
479
|
detail.hidden = false
|
|
301
480
|
const rect = panel.getBoundingClientRect()
|
|
@@ -305,17 +484,42 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
305
484
|
detail.style.left = (beside ? rect.right + 10 : Math.max(8, Math.min(rect.left, window.innerWidth - width - 8))) + 'px'
|
|
306
485
|
detail.style.top = Math.max(8, Math.min(beside ? rect.top : rect.bottom + 8, window.innerHeight - 240)) + 'px'
|
|
307
486
|
detail.showPopover?.()
|
|
487
|
+
schedulePosition()
|
|
488
|
+
}
|
|
489
|
+
const renderApprovalDetails = pending => {
|
|
490
|
+
if (pending?.kind !== 'approval') return
|
|
491
|
+
const details = approvalCallDetails(source?.getSnapshot(), pending)
|
|
492
|
+
let section = body.querySelector('[data-prts-approval-details]')
|
|
493
|
+
const signature = JSON.stringify(details ?? null)
|
|
494
|
+
if (section?.dataset.signature === signature) return
|
|
495
|
+
if (!section) { section = document.createElement('div'); section.setAttribute('data-prts-approval-details', ''); body.append(section) }
|
|
496
|
+
section.dataset.signature = signature
|
|
497
|
+
section.replaceChildren()
|
|
498
|
+
if (!details) {
|
|
499
|
+
section.textContent = source ? '暂无可用的操作参数,请进入会话核对详情。' : '正在读取操作详情…'
|
|
500
|
+
return
|
|
501
|
+
}
|
|
502
|
+
for (const [name, value] of [['命令', details.command], ['参数', details.parameters]]) {
|
|
503
|
+
if (!value) continue
|
|
504
|
+
const heading = document.createElement('div')
|
|
505
|
+
heading.textContent = name
|
|
506
|
+
const code = document.createElement('pre')
|
|
507
|
+
code.textContent = value
|
|
508
|
+
section.append(heading, code)
|
|
509
|
+
}
|
|
308
510
|
}
|
|
309
511
|
const render = () => {
|
|
310
512
|
if (active !== current) return
|
|
311
|
-
if (current.editor) { current.validate?.(); return }
|
|
513
|
+
if (current.editor) { current.validate?.(); if (pendingNow() === current.editingPending) renderApprovalDetails(current.editingPending); return }
|
|
312
514
|
const status = getSessionStatus?.()?.getSnapshot().get(id)
|
|
313
515
|
const pending = status?.pendingInteraction === current.answeredPending
|
|
314
516
|
? undefined : status?.pendingInteraction
|
|
315
517
|
const approval = pending?.kind === 'approval'
|
|
518
|
+
panel.toggleAttribute('data-prts-preview-approval', approval)
|
|
316
519
|
const question = approval || pending?.kind === 'question' || pending?.kind === 'plan-review'
|
|
317
520
|
const state = question ? 'warning' : status?.running || (current.answeredPending && status?.pendingInteraction === current.answeredPending) ? 'ongoing'
|
|
318
521
|
: status?.completionUnread || status?.running === false ? 'done' : current.group?.getAttribute('data-prts-session-summary-group') || ''
|
|
522
|
+
if (current.state && current.state !== state && !follows(current.state)) return
|
|
319
523
|
if (current.state && current.state !== state) {
|
|
320
524
|
current.sticky = true
|
|
321
525
|
keep()
|
|
@@ -346,39 +550,53 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
346
550
|
choices.hidden = !labels.length
|
|
347
551
|
panel.setAttribute('aria-label', title + ':' + label.textContent)
|
|
348
552
|
if (state === 'ongoing' && source) {
|
|
349
|
-
const replies =
|
|
553
|
+
const replies = selectReplies(source.getSnapshot(), replyLimit())
|
|
554
|
+
if (replies === historyKeys) return
|
|
350
555
|
panel.toggleAttribute('data-prts-preview-empty', !replies.length)
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
556
|
+
const previousTop = body.scrollTop
|
|
557
|
+
const scrollAnchor = previousTop > 0 ? [...body.children].find(entry => entry.getBoundingClientRect().bottom > body.getBoundingClientRect().top) : undefined
|
|
558
|
+
const anchorTop = scrollAnchor?.getBoundingClientRect().top
|
|
559
|
+
const retained = new Map(historyKeys.map(reply => [reply.key, reply]))
|
|
560
|
+
for (const old of historyKeys) {
|
|
561
|
+
const replacement = replies.find(reply => reply.key === old.key)
|
|
562
|
+
if (!replacement || replacement.text !== old.text || replacement.ordinal !== old.ordinal) {
|
|
563
|
+
body.querySelectorAll('[data-prts-preview-message]').forEach(entry => {
|
|
564
|
+
if (entry.dataset.replyKey === old.key) entry.remove()
|
|
565
|
+
})
|
|
566
|
+
retained.delete(old.key)
|
|
567
|
+
if (detailKey === old.key) hideDetail()
|
|
568
|
+
}
|
|
357
569
|
}
|
|
570
|
+
if (!historyKeys.length) body.replaceChildren()
|
|
358
571
|
if (!replies.length) body.textContent = '正在处理,等待本轮正文输出完成…'
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
572
|
+
for (let index = 0; index < replies.length; index++) {
|
|
573
|
+
const reply = replies[index]
|
|
574
|
+
if (retained.has(reply.key)) continue
|
|
362
575
|
const entry = panel.ownerDocument.createElement('article')
|
|
363
576
|
entry.setAttribute('data-prts-preview-message', '')
|
|
577
|
+
entry.dataset.replyKey = reply.key
|
|
364
578
|
const item = panel.ownerDocument.createElement('button')
|
|
365
579
|
item.type = 'button'
|
|
366
580
|
item.setAttribute('data-prts-reply-entry', '')
|
|
367
581
|
const heading = panel.ownerDocument.createElement('small')
|
|
368
|
-
heading.textContent = '回复 ' +
|
|
582
|
+
heading.textContent = '回复 ' + reply.ordinal
|
|
369
583
|
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)
|
|
584
|
+
summary.textContent = current.contentCache(reply.text).summary
|
|
373
585
|
item.append(heading, summary)
|
|
374
|
-
item.addEventListener('pointermove', () => showDetail(
|
|
375
|
-
item.addEventListener('focus', () => showDetail(
|
|
376
|
-
item.addEventListener('click', () => showDetail(
|
|
586
|
+
item.addEventListener('pointermove', () => showDetail(reply, index, item))
|
|
587
|
+
item.addEventListener('focus', () => showDetail(reply, index, item))
|
|
588
|
+
item.addEventListener('click', () => showDetail(reply, index, item))
|
|
377
589
|
entry.append(item)
|
|
378
590
|
body.prepend(entry)
|
|
379
591
|
}
|
|
380
|
-
|
|
381
|
-
|
|
592
|
+
// Reorder only when necessary, preserving retained DOM and open details.
|
|
593
|
+
const entries = new Map([...body.children].map(entry => [entry.dataset.replyKey, entry]))
|
|
594
|
+
for (let i = replies.length - 1, position = 0; i >= 0; i--, position++) {
|
|
595
|
+
const entry = entries.get(replies[i].key)
|
|
596
|
+
if (body.children[position] !== entry) body.insertBefore(entry, body.children[position] ?? null)
|
|
597
|
+
}
|
|
598
|
+
if (scrollAnchor?.isConnected) body.scrollTop += scrollAnchor.getBoundingClientRect().top - anchorTop
|
|
599
|
+
else if (previousTop === 0) body.scrollTop = 0
|
|
382
600
|
historyKeys = replies
|
|
383
601
|
previousText = undefined
|
|
384
602
|
previousQuestion = undefined
|
|
@@ -396,14 +614,21 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
396
614
|
if (previousText !== text || previousQuestion !== question) {
|
|
397
615
|
const extendsReply = previousQuestion === question && previousText && text.startsWith(previousText)
|
|
398
616
|
if (question) body.textContent = text
|
|
399
|
-
else body.innerHTML =
|
|
617
|
+
else body.innerHTML = current.contentCache(text).html
|
|
400
618
|
previousText = text
|
|
401
619
|
previousQuestion = question
|
|
402
620
|
if (!extendsReply) body.scrollTop = 0
|
|
403
621
|
}
|
|
622
|
+
if (approval) renderApprovalDetails(pending)
|
|
404
623
|
if (canAnswerPreview(pending)) enableAnswer()
|
|
405
624
|
}
|
|
406
|
-
|
|
625
|
+
let statusFields = previewStatusFields(getSessionStatus?.()?.getSnapshot().get(id))
|
|
626
|
+
current.unsubscribeStatus = getSessionStatus?.()?.subscribe(() => {
|
|
627
|
+
const next = previewStatusFields(getSessionStatus?.()?.getSnapshot().get(id))
|
|
628
|
+
if (next.every((value, index) => value === statusFields[index])) return
|
|
629
|
+
statusFields = next
|
|
630
|
+
render()
|
|
631
|
+
})
|
|
407
632
|
render()
|
|
408
633
|
body.scrollTop = 0
|
|
409
634
|
current.delay = window.setTimeout(async () => {
|
|
@@ -411,14 +636,16 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
411
636
|
const service = getUiConversation?.()
|
|
412
637
|
const controller = getSessions?.() ?? sessions
|
|
413
638
|
if (!controller?.retain || !service?.binding) throw new Error('unavailable')
|
|
414
|
-
current.reference = controller.retain(id, { source: 'prtsSummaryPreview' })
|
|
639
|
+
if (!current.reference) current.reference = controller.retain(id, { source: 'prtsSummaryPreview' })
|
|
415
640
|
const binding = await current.reference.ready
|
|
416
641
|
if (active !== current) return
|
|
417
|
-
source
|
|
642
|
+
source ??= service.binding(binding).target('chat')
|
|
643
|
+
current.source = source
|
|
418
644
|
current.unsubscribe = source.subscribe(() => {
|
|
419
645
|
if (active !== current || current.refresh !== undefined) return
|
|
420
646
|
current.refresh = window.setTimeout(() => {
|
|
421
647
|
current.refresh = undefined
|
|
648
|
+
if (current.state === 'ongoing' && !current.editor && selectReplies(source.getSnapshot(), replyLimit()) === historyKeys) return
|
|
422
649
|
render()
|
|
423
650
|
}, 120)
|
|
424
651
|
})
|
|
@@ -432,14 +659,14 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
432
659
|
render()
|
|
433
660
|
if (body.textContent.trim() === '正在读取…') body.textContent = '暂时无法读取回复,可点击会话查看'
|
|
434
661
|
}
|
|
435
|
-
}, 80)
|
|
662
|
+
}, cached ? 0 : 80)
|
|
436
663
|
}
|
|
437
664
|
function leave() {
|
|
438
665
|
if (!active || active.sticky) return
|
|
439
666
|
const current = active
|
|
440
667
|
window.clearTimeout(current.leaveTimer)
|
|
441
668
|
current.leaveTimer = window.setTimeout(() => {
|
|
442
|
-
if (active === current && !current.panel.matches(':hover')) stop()
|
|
669
|
+
if (active === current && !current.panel.matches(':hover')) stop(true, true)
|
|
443
670
|
}, 300)
|
|
444
671
|
}
|
|
445
672
|
function reanchor(root) {
|
|
@@ -455,7 +682,14 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
|
|
|
455
682
|
anchor?.setAttribute('data-prts-preview-active', '')
|
|
456
683
|
window.clearTimeout(active.leaveTimer)
|
|
457
684
|
}
|
|
458
|
-
|
|
685
|
+
function enterState(state) {
|
|
686
|
+
if (active && active.state !== state) stop(true, true)
|
|
687
|
+
}
|
|
688
|
+
function pinnedState() {
|
|
689
|
+
return active && !follows(active.state)
|
|
690
|
+
? { id: active.id, state: active.state } : undefined
|
|
691
|
+
}
|
|
692
|
+
return { show, stop, leave, reanchor, enterState, pinnedState, isOpen: () => Boolean(active) }
|
|
459
693
|
}
|
|
460
694
|
|
|
461
695
|
export function scrollSummaryTitle(button, viewport, text, window) {
|