@ash-qw/dsh-theme-prts 1.3.0 → 1.4.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 +20 -0
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/THIRD_PARTY_NOTICES.zh-CN.md +7 -0
- package/lib/client.js +882 -41
- package/package.json +3 -2
- package/src/client/assistant-glass-adapter.js +68 -4
- package/src/client/conversation-control-adapter.js +2 -0
- package/src/client/cursor-adapter.js +19 -2
- package/src/client/custom-emblem.js +1 -1
- package/src/client/facility-geometry.js +14 -10
- package/src/client/facility-vector.js +1 -1
- package/src/client/index.js +4 -2
- package/src/client/particle-field-adapter.js +18 -9
- package/src/client/preferences.js +17 -2
- package/src/client/preview-markdown.js +18 -0
- package/src/client/session-summary-preview.js +479 -0
- package/src/client/sidebar-control-adapter.js +52 -6
- package/src/client/theme-settings-workbench.js +39 -2
- package/src/styles/facility-vector.css +91 -3
- package/src/styles/prts.css +27 -13
- package/src/styles/refinement.css +34 -15
- package/src/styles/theme-settings-workbench.css +20 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { renderPreviewMarkdown } from './preview-markdown.js'
|
|
2
|
+
export function canAnswerPreview(pending) {
|
|
3
|
+
if (pending?.kind === 'approval') return pending.answerable === true && typeof pending.answer === 'function'
|
|
4
|
+
const question = pending?.questions?.[0]
|
|
5
|
+
return pending?.kind === 'question' && pending.questions.length === 1
|
|
6
|
+
&& typeof pending.answer === 'function' && typeof question?.id === 'string'
|
|
7
|
+
&& !question.intent && !question.detail && question.options?.length > 0
|
|
8
|
+
&& !pending.getSnapshot?.().closed
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Pending choices are separate from assistant prose in DSH.
|
|
12
|
+
export function pendingQuestionPreview(pending) {
|
|
13
|
+
const questions = pending.questions ?? []
|
|
14
|
+
const text = questions.slice(0, 2).map(item => {
|
|
15
|
+
const question = (item.question ?? '').slice(0, 80)
|
|
16
|
+
const options = (item.options ?? []).slice(0, 4).map(option => option.label).filter(Boolean).join(' / ')
|
|
17
|
+
return [question, options].filter(Boolean).join(':')
|
|
18
|
+
}).join(';')
|
|
19
|
+
return text ? text.slice(0, 240) : '有待回答的问题,请打开会话查看'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Read the host's assembled Chat view rather than interpreting raw stream events.
|
|
23
|
+
export function latestAssistantReply(snapshot) {
|
|
24
|
+
const order = snapshot?.order ?? []
|
|
25
|
+
const latestTurn = snapshot?.timeline?.turnOrder?.at(-1)
|
|
26
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
27
|
+
const node = snapshot.nodes?.get?.(order[i])
|
|
28
|
+
// Never search past a new user input, even before turn/start arrives.
|
|
29
|
+
if (['user', 'steering', 'turn-trigger'].includes(node?.kind)) return ''
|
|
30
|
+
if (latestTurn !== undefined && node?.location?.turn?.turn !== latestTurn) continue
|
|
31
|
+
if (node?.kind !== 'assistant-step' || node.visibility === 'hidden') continue
|
|
32
|
+
let text = ''
|
|
33
|
+
for (const block of node.data?.blocks ?? []) {
|
|
34
|
+
if (block.kind === 'text') text += block.text ?? ''
|
|
35
|
+
}
|
|
36
|
+
text = text.trim()
|
|
37
|
+
if (text) return text
|
|
38
|
+
}
|
|
39
|
+
return ''
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function completedPreviewReplies(snapshot) {
|
|
43
|
+
const replies = []
|
|
44
|
+
const latestTurn = snapshot?.timeline?.turnOrder?.at(-1)
|
|
45
|
+
for (const key of [...(snapshot?.order ?? [])].reverse()) {
|
|
46
|
+
const node = snapshot.nodes?.get?.(key)
|
|
47
|
+
if (['user', 'steering', 'turn-trigger'].includes(node?.kind)) break
|
|
48
|
+
if (latestTurn !== undefined && node?.location?.turn?.turn !== latestTurn) continue
|
|
49
|
+
if (node?.kind !== 'assistant-step' || node.visibility === 'hidden' || node.data?.status !== 'settled') continue
|
|
50
|
+
const text = (node.data.blocks ?? []).filter(block => block.kind === 'text').map(block => block.text ?? '').join('').trim()
|
|
51
|
+
if (text) replies.push({key, text})
|
|
52
|
+
}
|
|
53
|
+
return replies.reverse()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createSessionSummaryPreview({ document, window, sessions, getSessions, getUiConversation, getSessionStatus, openSession, getPreviewPreferences }) {
|
|
57
|
+
let active
|
|
58
|
+
function stop(force = true) {
|
|
59
|
+
if (!force && active) return
|
|
60
|
+
const old = active
|
|
61
|
+
active = undefined
|
|
62
|
+
if (!old) return
|
|
63
|
+
window.clearTimeout(old.leaveTimer)
|
|
64
|
+
window.clearTimeout(old.delay)
|
|
65
|
+
window.clearTimeout(old.refresh)
|
|
66
|
+
old.unsubscribe?.()
|
|
67
|
+
old.unsubscribeStatus?.()
|
|
68
|
+
old.reference?.release()
|
|
69
|
+
old.panel.hidePopover?.()
|
|
70
|
+
old.panel.hidden = true
|
|
71
|
+
old.panel.removeAttribute('data-prts-preview-editing')
|
|
72
|
+
old.panel.setAttribute('role', 'note')
|
|
73
|
+
old.cleanupEditor?.()
|
|
74
|
+
old.cleanupHover?.()
|
|
75
|
+
old.detail?.hidePopover?.()
|
|
76
|
+
old.detail?.remove()
|
|
77
|
+
old.group?.removeAttribute('data-prts-preview-open')
|
|
78
|
+
old.anchor?.removeAttribute('data-prts-preview-active')
|
|
79
|
+
if (old.detached) {
|
|
80
|
+
old.panel.remove()
|
|
81
|
+
if (old.originalParent?.isConnected) old.originalParent.append(old.panel)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function show(id, panel, title, anchor) {
|
|
85
|
+
if (active?.sticky && active.id !== id) return
|
|
86
|
+
if (active?.id === id) {
|
|
87
|
+
window.clearTimeout(active.leaveTimer)
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
stop()
|
|
91
|
+
const current = { id, panel, anchor, originalParent: panel.parentElement,
|
|
92
|
+
group: anchor?.closest('[data-prts-session-summary-group]') }
|
|
93
|
+
current.group?.setAttribute('data-prts-preview-open', '')
|
|
94
|
+
anchor?.setAttribute('data-prts-preview-active', '')
|
|
95
|
+
active = current
|
|
96
|
+
const keep = () => window.clearTimeout(current.leaveTimer)
|
|
97
|
+
const onDoubleClick = event => {
|
|
98
|
+
if (active !== current || event.button !== 0 || getPreviewPreferences?.().previewDoubleClick === false) return
|
|
99
|
+
if (event.target.closest?.('button, a, input, textarea, select, label, summary, [role="button"], [role="link"], [contenteditable]:not([contenteditable="false"])')) return
|
|
100
|
+
event.preventDefault()
|
|
101
|
+
event.stopPropagation()
|
|
102
|
+
stop()
|
|
103
|
+
openSession?.(id)
|
|
104
|
+
}
|
|
105
|
+
const dismissSticky = event => {
|
|
106
|
+
if (!current.sticky) return
|
|
107
|
+
if (event.type === 'keydown') {
|
|
108
|
+
if (event.key !== 'Escape') return
|
|
109
|
+
event.preventDefault()
|
|
110
|
+
} else if (panel.contains(event.target) || current.anchor?.contains(event.target)) return
|
|
111
|
+
stop()
|
|
112
|
+
}
|
|
113
|
+
panel.ownerDocument.addEventListener('pointerdown', dismissSticky)
|
|
114
|
+
panel.ownerDocument.addEventListener('keydown', dismissSticky)
|
|
115
|
+
panel.addEventListener('dblclick', onDoubleClick)
|
|
116
|
+
panel.addEventListener('pointerenter', keep)
|
|
117
|
+
panel.addEventListener('pointerleave', leave)
|
|
118
|
+
current.cleanupHover = () => {
|
|
119
|
+
panel.ownerDocument.removeEventListener('pointerdown', dismissSticky)
|
|
120
|
+
panel.ownerDocument.removeEventListener('keydown', dismissSticky)
|
|
121
|
+
panel.removeEventListener('dblclick', onDoubleClick)
|
|
122
|
+
panel.removeEventListener('pointerenter', keep)
|
|
123
|
+
panel.removeEventListener('pointerleave', leave)
|
|
124
|
+
}
|
|
125
|
+
panel.removeAttribute('data-prts-preview-editing')
|
|
126
|
+
panel.setAttribute('role', 'note')
|
|
127
|
+
panel.hidden = false
|
|
128
|
+
if (anchor) {
|
|
129
|
+
const list = anchor.closest('[data-prts-session-summary-popover]').getBoundingClientRect()
|
|
130
|
+
const row = anchor.getBoundingClientRect()
|
|
131
|
+
panel.style.left = (list.right + 10) + 'px'
|
|
132
|
+
panel.style.top = Math.max(8, Math.min(row.top, window.innerHeight - 230)) + 'px'
|
|
133
|
+
}
|
|
134
|
+
panel.ownerDocument.body.append(panel)
|
|
135
|
+
current.detached = true
|
|
136
|
+
panel.showPopover?.()
|
|
137
|
+
const label = panel.querySelector('strong')
|
|
138
|
+
let body = panel.querySelector('[data-prts-preview-body]')
|
|
139
|
+
if (!body) {
|
|
140
|
+
body = panel.ownerDocument.createElement('div')
|
|
141
|
+
body.setAttribute('data-prts-preview-body', '')
|
|
142
|
+
panel.querySelector('p').replaceWith(body)
|
|
143
|
+
}
|
|
144
|
+
const choices = panel.ownerDocument.createElement('div')
|
|
145
|
+
choices.setAttribute('data-prts-preview-choices', '')
|
|
146
|
+
panel.querySelector('[data-prts-preview-choices]')?.remove()
|
|
147
|
+
panel.append(choices)
|
|
148
|
+
panel.querySelector('[data-prts-preview-actions]')?.remove()
|
|
149
|
+
const actions = panel.ownerDocument.createElement('div')
|
|
150
|
+
actions.setAttribute('data-prts-preview-actions', '')
|
|
151
|
+
panel.append(actions)
|
|
152
|
+
const pendingNow = () => getSessionStatus?.()?.getSnapshot().get(id)?.pendingInteraction
|
|
153
|
+
const button = (text, action) => {
|
|
154
|
+
const el = panel.ownerDocument.createElement('button')
|
|
155
|
+
el.type = 'button'
|
|
156
|
+
el.textContent = text
|
|
157
|
+
el.addEventListener('click', action)
|
|
158
|
+
return el
|
|
159
|
+
}
|
|
160
|
+
const closeSticky = button('关闭预览', () => stop())
|
|
161
|
+
const enter = button('进入会话', () => { stop(); openSession?.(id) })
|
|
162
|
+
const enableAnswer = () => {
|
|
163
|
+
const pending = pendingNow()
|
|
164
|
+
if (!canAnswerPreview(pending)) return
|
|
165
|
+
current.editor = true
|
|
166
|
+
// Detach from the hover menu before promoting again to the top layer.
|
|
167
|
+
panel.setAttribute('data-prts-preview-editing', '')
|
|
168
|
+
panel.setAttribute('role', 'dialog')
|
|
169
|
+
|
|
170
|
+
const approval = pending.kind === 'approval'
|
|
171
|
+
const question = approval ? {
|
|
172
|
+
options: [{label:'仅允许本次'}, {label:'拒绝'}]
|
|
173
|
+
} : pending.questions[0]
|
|
174
|
+
const submitLabel = approval ? '提交审批' : '提交回答'
|
|
175
|
+
const selected = new Set()
|
|
176
|
+
let busy = false
|
|
177
|
+
const message = panel.ownerDocument.createElement('small')
|
|
178
|
+
message.setAttribute('role', 'status')
|
|
179
|
+
const submit = button(submitLabel, async () => {
|
|
180
|
+
if (busy || !selected.size) return
|
|
181
|
+
if (pendingNow() !== pending || pending.getSnapshot?.().closed || (approval && !pending.answerable)) {
|
|
182
|
+
message.textContent = '问题状态已变化,请重新打开预览。'
|
|
183
|
+
submit.disabled = true
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
busy = true
|
|
187
|
+
submit.disabled = true
|
|
188
|
+
submit.textContent = '正在提交…'
|
|
189
|
+
try {
|
|
190
|
+
const channel = pending.getSnapshot?.().channel
|
|
191
|
+
await pending.answer(approval
|
|
192
|
+
? selected.has('仅允许本次') ? 'allowed-once' : 'rejected'
|
|
193
|
+
: {answers:[{id:question.id,selected:question.options.map(o=>o.label).filter(value=>selected.has(value))}]})
|
|
194
|
+
if (active === current) {
|
|
195
|
+
current.sticky = true
|
|
196
|
+
keep()
|
|
197
|
+
current.answeredPending = pending
|
|
198
|
+
current.cleanupEditor?.()
|
|
199
|
+
current.cleanupEditor = undefined
|
|
200
|
+
current.validate = undefined
|
|
201
|
+
current.editor = false
|
|
202
|
+
panel.removeAttribute('data-prts-preview-editing')
|
|
203
|
+
panel.setAttribute('role', 'note')
|
|
204
|
+
render()
|
|
205
|
+
}
|
|
206
|
+
if (channel === 'rpc') await pending.dismiss?.()
|
|
207
|
+
} catch {
|
|
208
|
+
if (active !== current) return
|
|
209
|
+
busy = false
|
|
210
|
+
submit.disabled = false
|
|
211
|
+
submit.textContent = submitLabel
|
|
212
|
+
message.textContent = '提交失败,请重试或进入会话处理。'
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
submit.disabled = true
|
|
216
|
+
choices.replaceChildren(...question.options.map(option => {
|
|
217
|
+
const choice = button(option.label, () => {
|
|
218
|
+
if (busy) return
|
|
219
|
+
if (question.multiSelect) {
|
|
220
|
+
if (selected.has(option.label)) selected.delete(option.label)
|
|
221
|
+
else selected.add(option.label)
|
|
222
|
+
} else {
|
|
223
|
+
selected.clear()
|
|
224
|
+
selected.add(option.label)
|
|
225
|
+
}
|
|
226
|
+
pending.engage?.()
|
|
227
|
+
for (const el of choices.children) el.setAttribute('aria-pressed', String(selected.has(el.dataset.value)))
|
|
228
|
+
submit.disabled = !selected.size
|
|
229
|
+
})
|
|
230
|
+
choice.dataset.value = option.label
|
|
231
|
+
choice.setAttribute('aria-pressed','false')
|
|
232
|
+
if (option.description) {
|
|
233
|
+
const detail = panel.ownerDocument.createElement('small')
|
|
234
|
+
detail.textContent = option.description
|
|
235
|
+
choice.append(detail)
|
|
236
|
+
}
|
|
237
|
+
return choice
|
|
238
|
+
}))
|
|
239
|
+
choices.hidden = false
|
|
240
|
+
actions.replaceChildren(button('取消', () => stop()), enter, submit, message)
|
|
241
|
+
const onKey = event => { if (event.key === 'Escape') { event.preventDefault(); stop() } }
|
|
242
|
+
panel.addEventListener('keydown', onKey)
|
|
243
|
+
const onEnter = () => pending.holdFocus?.()
|
|
244
|
+
const onOutside = event => { if (!panel.contains(event.target) && !anchor?.contains(event.target)) stop() }
|
|
245
|
+
panel.addEventListener('pointerenter', onEnter)
|
|
246
|
+
panel.ownerDocument.addEventListener('pointerdown', onOutside)
|
|
247
|
+
current.cleanupEditor = () => {
|
|
248
|
+
pending.releaseFocus?.()
|
|
249
|
+
panel.removeEventListener('keydown', onKey)
|
|
250
|
+
panel.removeEventListener('pointerenter', onEnter)
|
|
251
|
+
panel.ownerDocument.removeEventListener('pointerdown', onOutside)
|
|
252
|
+
}
|
|
253
|
+
current.validate = () => {
|
|
254
|
+
if (!busy && (pendingNow() !== pending || (approval && !pending.answerable))) {
|
|
255
|
+
message.textContent = '问题已更新或结束,请进入会话查看。'
|
|
256
|
+
submit.disabled = true
|
|
257
|
+
for (const el of choices.children) el.disabled = true
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
let lastChoices = ''
|
|
262
|
+
let source
|
|
263
|
+
let previousText
|
|
264
|
+
let previousQuestion
|
|
265
|
+
let historyKeys = []
|
|
266
|
+
let detailKey
|
|
267
|
+
let detailEntry
|
|
268
|
+
const hideDetail = () => {
|
|
269
|
+
current.detail?.hidePopover?.()
|
|
270
|
+
if (current.detail) current.detail.hidden = true
|
|
271
|
+
detailKey = undefined
|
|
272
|
+
detailEntry?.removeAttribute('data-prts-reply-active')
|
|
273
|
+
detailEntry = undefined
|
|
274
|
+
}
|
|
275
|
+
const showDetail = (reply, index, item) => {
|
|
276
|
+
keep()
|
|
277
|
+
if (detailKey === reply.key && current.detail && !current.detail.hidden) return
|
|
278
|
+
if (!current.detail) {
|
|
279
|
+
const detail = panel.ownerDocument.createElement('section')
|
|
280
|
+
detail.setAttribute('data-prts-summary-reply', '')
|
|
281
|
+
detail.setAttribute('data-prts-reply-detail', '')
|
|
282
|
+
detail.setAttribute('popover', 'manual')
|
|
283
|
+
detail.setAttribute('role', 'note')
|
|
284
|
+
const heading = panel.ownerDocument.createElement('strong')
|
|
285
|
+
const content = panel.ownerDocument.createElement('div')
|
|
286
|
+
content.setAttribute('data-prts-preview-body', '')
|
|
287
|
+
detail.append(heading, content)
|
|
288
|
+
panel.append(detail)
|
|
289
|
+
current.detail = detail
|
|
290
|
+
}
|
|
291
|
+
const detail = current.detail
|
|
292
|
+
detailKey = reply.key
|
|
293
|
+
detailEntry?.removeAttribute('data-prts-reply-active')
|
|
294
|
+
detailEntry = item
|
|
295
|
+
item?.setAttribute('data-prts-reply-active', '')
|
|
296
|
+
detail.querySelector('strong').textContent = '回复 ' + (index + 1)
|
|
297
|
+
const content = detail.querySelector('[data-prts-preview-body]')
|
|
298
|
+
content.innerHTML = renderPreviewMarkdown(reply.text)
|
|
299
|
+
content.scrollTop = 0
|
|
300
|
+
detail.hidden = false
|
|
301
|
+
const rect = panel.getBoundingClientRect()
|
|
302
|
+
const width = Math.min(340, window.innerWidth - 16)
|
|
303
|
+
detail.style.width = width + 'px'
|
|
304
|
+
const beside = rect.right + 10 + width <= window.innerWidth - 8
|
|
305
|
+
detail.style.left = (beside ? rect.right + 10 : Math.max(8, Math.min(rect.left, window.innerWidth - width - 8))) + 'px'
|
|
306
|
+
detail.style.top = Math.max(8, Math.min(beside ? rect.top : rect.bottom + 8, window.innerHeight - 240)) + 'px'
|
|
307
|
+
detail.showPopover?.()
|
|
308
|
+
}
|
|
309
|
+
const render = () => {
|
|
310
|
+
if (active !== current) return
|
|
311
|
+
if (current.editor) { current.validate?.(); return }
|
|
312
|
+
const status = getSessionStatus?.()?.getSnapshot().get(id)
|
|
313
|
+
const pending = status?.pendingInteraction === current.answeredPending
|
|
314
|
+
? undefined : status?.pendingInteraction
|
|
315
|
+
const approval = pending?.kind === 'approval'
|
|
316
|
+
const question = approval || pending?.kind === 'question' || pending?.kind === 'plan-review'
|
|
317
|
+
const state = question ? 'warning' : status?.running || (current.answeredPending && status?.pendingInteraction === current.answeredPending) ? 'ongoing'
|
|
318
|
+
: status?.completionUnread || status?.running === false ? 'done' : current.group?.getAttribute('data-prts-session-summary-group') || ''
|
|
319
|
+
if (current.state && current.state !== state) {
|
|
320
|
+
current.sticky = true
|
|
321
|
+
keep()
|
|
322
|
+
}
|
|
323
|
+
current.state = state
|
|
324
|
+
panel.setAttribute('data-prts-preview-state', state)
|
|
325
|
+
label.textContent = approval ? '等待审批 · 仅本次授权' : question ? '等待你的选择' : state === 'ongoing' ? '运行中 · 已完成正文' : state === 'done' ? '已完成 · 最后回复' : '最新 AI 回复'
|
|
326
|
+
panel.toggleAttribute('data-prts-preview-question', question)
|
|
327
|
+
if (question) {
|
|
328
|
+
if (!actions.contains(enter)) actions.replaceChildren(enter)
|
|
329
|
+
} else if (current.sticky) {
|
|
330
|
+
if (!actions.contains(closeSticky)) actions.replaceChildren(closeSticky)
|
|
331
|
+
} else actions.replaceChildren()
|
|
332
|
+
const first = question ? pending.questions?.[0] : undefined
|
|
333
|
+
const labels = (first?.options ?? []).slice(0, 3).map(option => option.label)
|
|
334
|
+
const remaining = Math.max(0, (first?.options?.length ?? 0) - 3)
|
|
335
|
+
if (remaining) labels.push('另有 ' + remaining + ' 个选项')
|
|
336
|
+
if ((pending?.questions?.length ?? 0) > 1) labels.push('共 ' + pending.questions.length + ' 个问题')
|
|
337
|
+
const signature = JSON.stringify(labels)
|
|
338
|
+
if (signature !== lastChoices) {
|
|
339
|
+
lastChoices = signature
|
|
340
|
+
choices.replaceChildren(...labels.map(text => {
|
|
341
|
+
const option = panel.ownerDocument.createElement('span')
|
|
342
|
+
option.textContent = text
|
|
343
|
+
return option
|
|
344
|
+
}))
|
|
345
|
+
}
|
|
346
|
+
choices.hidden = !labels.length
|
|
347
|
+
panel.setAttribute('aria-label', title + ':' + label.textContent)
|
|
348
|
+
if (state === 'ongoing' && source) {
|
|
349
|
+
const replies = completedPreviewReplies(source.getSnapshot())
|
|
350
|
+
panel.toggleAttribute('data-prts-preview-empty', !replies.length)
|
|
351
|
+
const extendsHistory = historyKeys.length > 0 && historyKeys.every((entry,index) => replies[index]?.key === entry.key && replies[index]?.text === entry.text)
|
|
352
|
+
if (!extendsHistory) {
|
|
353
|
+
hideDetail()
|
|
354
|
+
body.replaceChildren()
|
|
355
|
+
body.scrollTop = 0
|
|
356
|
+
historyKeys = []
|
|
357
|
+
}
|
|
358
|
+
if (!replies.length) body.textContent = '正在处理,等待本轮正文输出完成…'
|
|
359
|
+
const previousHeight = body.scrollHeight
|
|
360
|
+
const previousTop = body.scrollTop
|
|
361
|
+
for (let index = historyKeys.length; index < replies.length; index++) {
|
|
362
|
+
const entry = panel.ownerDocument.createElement('article')
|
|
363
|
+
entry.setAttribute('data-prts-preview-message', '')
|
|
364
|
+
const item = panel.ownerDocument.createElement('button')
|
|
365
|
+
item.type = 'button'
|
|
366
|
+
item.setAttribute('data-prts-reply-entry', '')
|
|
367
|
+
const heading = panel.ownerDocument.createElement('small')
|
|
368
|
+
heading.textContent = '回复 ' + (index + 1)
|
|
369
|
+
const summary = panel.ownerDocument.createElement('span')
|
|
370
|
+
const plain = panel.ownerDocument.createElement('div')
|
|
371
|
+
plain.innerHTML = renderPreviewMarkdown(replies[index].text)
|
|
372
|
+
summary.textContent = plain.textContent.replace(/\s+/g, ' ').trim().slice(0, 100)
|
|
373
|
+
item.append(heading, summary)
|
|
374
|
+
item.addEventListener('pointermove', () => showDetail(replies[index], index, item))
|
|
375
|
+
item.addEventListener('focus', () => showDetail(replies[index], index, item))
|
|
376
|
+
item.addEventListener('click', () => showDetail(replies[index], index, item))
|
|
377
|
+
entry.append(item)
|
|
378
|
+
body.prepend(entry)
|
|
379
|
+
}
|
|
380
|
+
if (previousTop > 0) body.scrollTop = previousTop + body.scrollHeight - previousHeight
|
|
381
|
+
else body.scrollTop = 0
|
|
382
|
+
historyKeys = replies
|
|
383
|
+
previousText = undefined
|
|
384
|
+
previousQuestion = undefined
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
hideDetail()
|
|
388
|
+
historyKeys = []
|
|
389
|
+
const excerpt = source ? latestAssistantReply(source.getSnapshot()) : ''
|
|
390
|
+
panel.toggleAttribute('data-prts-preview-empty', !question && !excerpt)
|
|
391
|
+
const text = question
|
|
392
|
+
? approval
|
|
393
|
+
? [pending.toolName, pending.displayReason?.['zh-CN'] || pending.displayReason?.zh || pending.reason || pending.displayReason?.en || '请进入会话查看操作详情'].filter(Boolean).join(':')
|
|
394
|
+
: first?.question || pendingQuestionPreview(pending)
|
|
395
|
+
: source ? excerpt || (state === 'ongoing' ? '正在处理,尚未输出正文…' : '暂无正文回复,可进入会话查看执行记录。') : '正在读取…'
|
|
396
|
+
if (previousText !== text || previousQuestion !== question) {
|
|
397
|
+
const extendsReply = previousQuestion === question && previousText && text.startsWith(previousText)
|
|
398
|
+
if (question) body.textContent = text
|
|
399
|
+
else body.innerHTML = renderPreviewMarkdown(text)
|
|
400
|
+
previousText = text
|
|
401
|
+
previousQuestion = question
|
|
402
|
+
if (!extendsReply) body.scrollTop = 0
|
|
403
|
+
}
|
|
404
|
+
if (canAnswerPreview(pending)) enableAnswer()
|
|
405
|
+
}
|
|
406
|
+
current.unsubscribeStatus = getSessionStatus?.()?.subscribe(render)
|
|
407
|
+
render()
|
|
408
|
+
body.scrollTop = 0
|
|
409
|
+
current.delay = window.setTimeout(async () => {
|
|
410
|
+
try {
|
|
411
|
+
const service = getUiConversation?.()
|
|
412
|
+
const controller = getSessions?.() ?? sessions
|
|
413
|
+
if (!controller?.retain || !service?.binding) throw new Error('unavailable')
|
|
414
|
+
current.reference = controller.retain(id, { source: 'prtsSummaryPreview' })
|
|
415
|
+
const binding = await current.reference.ready
|
|
416
|
+
if (active !== current) return
|
|
417
|
+
source = service.binding(binding).target('chat')
|
|
418
|
+
current.unsubscribe = source.subscribe(() => {
|
|
419
|
+
if (active !== current || current.refresh !== undefined) return
|
|
420
|
+
current.refresh = window.setTimeout(() => {
|
|
421
|
+
current.refresh = undefined
|
|
422
|
+
render()
|
|
423
|
+
}, 120)
|
|
424
|
+
})
|
|
425
|
+
render()
|
|
426
|
+
} catch {
|
|
427
|
+
if (active !== current) return
|
|
428
|
+
current.unsubscribe?.()
|
|
429
|
+
current.unsubscribe = undefined
|
|
430
|
+
current.reference?.release()
|
|
431
|
+
current.reference = undefined
|
|
432
|
+
render()
|
|
433
|
+
if (body.textContent.trim() === '正在读取…') body.textContent = '暂时无法读取回复,可点击会话查看'
|
|
434
|
+
}
|
|
435
|
+
}, 80)
|
|
436
|
+
}
|
|
437
|
+
function leave() {
|
|
438
|
+
if (!active || active.sticky) return
|
|
439
|
+
const current = active
|
|
440
|
+
window.clearTimeout(current.leaveTimer)
|
|
441
|
+
current.leaveTimer = window.setTimeout(() => {
|
|
442
|
+
if (active === current && !current.panel.matches(':hover')) stop()
|
|
443
|
+
}, 300)
|
|
444
|
+
}
|
|
445
|
+
function reanchor(root) {
|
|
446
|
+
if (!active) return
|
|
447
|
+
const anchor = [...root.querySelectorAll('[data-prts-session-summary-item]')]
|
|
448
|
+
.find(item => item.getAttribute('data-prts-session-summary-item') === active.id)
|
|
449
|
+
active.group?.removeAttribute('data-prts-preview-open')
|
|
450
|
+
active.anchor?.removeAttribute('data-prts-preview-active')
|
|
451
|
+
active.anchor = anchor
|
|
452
|
+
active.group = anchor?.closest('[data-prts-session-summary-group]')
|
|
453
|
+
active.originalParent = undefined
|
|
454
|
+
active.group?.setAttribute('data-prts-preview-open', '')
|
|
455
|
+
anchor?.setAttribute('data-prts-preview-active', '')
|
|
456
|
+
window.clearTimeout(active.leaveTimer)
|
|
457
|
+
}
|
|
458
|
+
return { show, stop, leave, reanchor }
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function scrollSummaryTitle(button, viewport, text, window) {
|
|
462
|
+
let animation
|
|
463
|
+
const stop = () => { animation?.cancel(); animation = undefined }
|
|
464
|
+
const start = () => {
|
|
465
|
+
stop()
|
|
466
|
+
const distance = text.scrollWidth - viewport.clientWidth
|
|
467
|
+
if (distance <= 8 || !text.animate) return
|
|
468
|
+
const reduced = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
469
|
+
animation = text.animate([
|
|
470
|
+
{ transform: 'translateX(0)' },
|
|
471
|
+
{ transform: `translateX(-${distance}px)` },
|
|
472
|
+
], { duration: reduced ? 0 : distance / 30 * 1000, fill: 'forwards', easing: 'linear' })
|
|
473
|
+
}
|
|
474
|
+
button.addEventListener('pointerenter', start)
|
|
475
|
+
button.addEventListener('pointerleave', stop)
|
|
476
|
+
button.addEventListener('focus', start)
|
|
477
|
+
button.addEventListener('blur', stop)
|
|
478
|
+
return stop
|
|
479
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createSessionSummaryPreview, scrollSummaryTitle } from './session-summary-preview.js'
|
|
1
2
|
import { ensureFacilityGraphic, ensureFacilityTextureDefs, renderFacilityGraphic } from './facility-vector.js'
|
|
2
3
|
import { createSessionLifelineAnimator } from './session-lifeline.js'
|
|
3
4
|
|
|
@@ -189,7 +190,8 @@ function ensureFacilitySpine(document, row, kind, next) {
|
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
|
|
192
|
-
export function createSidebarControlAdapter({ document, window, sessions, workspaces, getSessionStatus, getUiWorkspace }) {
|
|
193
|
+
export function createSidebarControlAdapter({ document, window, sessions, workspaces, getSessionStatus, getUiWorkspace, getUiConversation, getSessions }) {
|
|
194
|
+
let previewPreferences = {}
|
|
193
195
|
let observer
|
|
194
196
|
let resizeObserver
|
|
195
197
|
let resizeFrame
|
|
@@ -201,6 +203,9 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
201
203
|
let statusSourcesStarted = false
|
|
202
204
|
let summaryRoot
|
|
203
205
|
let summarySignature = ''
|
|
206
|
+
const preview = createSessionSummaryPreview({ document, window, sessions, getUiConversation, getSessions, getSessionStatus, openSession, getPreviewPreferences: () => previewPreferences })
|
|
207
|
+
let stopTitles = []
|
|
208
|
+
function clearSummaryEffects() { preview.stop(false); for (const stop of stopTitles) stop(); stopTitles = [] }
|
|
204
209
|
let owned = new Set()
|
|
205
210
|
let vectorOwners = new Set()
|
|
206
211
|
const dirtyVectors = new Map()
|
|
@@ -269,6 +274,19 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
269
274
|
hint.textContent = '选择会话'
|
|
270
275
|
heading.append(headingLabel, hint)
|
|
271
276
|
popover.append(heading)
|
|
277
|
+
const reply = document.createElement('section')
|
|
278
|
+
reply.setAttribute('data-prts-summary-reply', '')
|
|
279
|
+
reply.setAttribute('role', 'note')
|
|
280
|
+
reply.setAttribute('popover', 'manual')
|
|
281
|
+
reply.hidden = true
|
|
282
|
+
reply.append(document.createElement('strong'), document.createElement('p'))
|
|
283
|
+
group.addEventListener('pointerleave', () => preview.leave())
|
|
284
|
+
group.addEventListener('focusout', event => {
|
|
285
|
+
if (!group.contains(event.relatedTarget)) preview.leave()
|
|
286
|
+
})
|
|
287
|
+
const list = document.createElement('span')
|
|
288
|
+
list.setAttribute('data-prts-summary-list', '')
|
|
289
|
+
popover.append(list)
|
|
272
290
|
for (const item of items) {
|
|
273
291
|
const button = document.createElement('button')
|
|
274
292
|
button.type = 'button'
|
|
@@ -276,8 +294,21 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
276
294
|
button.setAttribute('data-prts-session-summary-item', item.id)
|
|
277
295
|
button.append(statusSignal(document, state))
|
|
278
296
|
const title = document.createElement('span')
|
|
279
|
-
title.
|
|
297
|
+
title.setAttribute('data-prts-summary-title', '')
|
|
298
|
+
const text = document.createElement('span')
|
|
299
|
+
text.textContent = item.title
|
|
300
|
+
title.append(text)
|
|
280
301
|
button.append(title)
|
|
302
|
+
button.setAttribute('aria-label', item.title)
|
|
303
|
+
stopTitles.push(scrollSummaryTitle(button, title, text, window))
|
|
304
|
+
const showReply = () => {
|
|
305
|
+
const key = {ongoing:'previewOngoing',warning:'previewWarning',done:'previewDone'}[state]
|
|
306
|
+
if (previewPreferences[key] === false) { preview.stop(); return }
|
|
307
|
+
preview.show(item.id, reply, item.title, button)
|
|
308
|
+
}
|
|
309
|
+
button.addEventListener('pointerenter', showReply)
|
|
310
|
+
button.addEventListener('pointerleave', () => preview.leave())
|
|
311
|
+
button.addEventListener('focus', showReply)
|
|
281
312
|
if (item.statusLabel) {
|
|
282
313
|
const detail = document.createElement('small')
|
|
283
314
|
detail.setAttribute('data-prts-session-summary-item-status', '')
|
|
@@ -287,17 +318,21 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
287
318
|
}
|
|
288
319
|
button.addEventListener('click', event => {
|
|
289
320
|
event.stopPropagation()
|
|
321
|
+
preview.stop()
|
|
290
322
|
openSession(item.id)
|
|
291
323
|
})
|
|
292
|
-
|
|
324
|
+
list.append(button)
|
|
293
325
|
}
|
|
326
|
+
popover.append(reply)
|
|
294
327
|
group.append(trigger, popover)
|
|
295
328
|
return group
|
|
296
329
|
}
|
|
297
330
|
|
|
298
331
|
function syncSummary(root, next) {
|
|
299
332
|
const { header, label } = resolveSectionHeader(root)
|
|
300
|
-
if (!header || !label) {
|
|
333
|
+
if (previewPreferences.sessionPreview === false || !header || !label) {
|
|
334
|
+
preview.stop()
|
|
335
|
+
clearSummaryEffects()
|
|
301
336
|
summaryRoot?.remove()
|
|
302
337
|
summaryRoot = undefined
|
|
303
338
|
summarySignature = ''
|
|
@@ -349,10 +384,12 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
349
384
|
summaryRoot.hidden = warning.length === 0 && ongoing.length === 0 && done.length === 0
|
|
350
385
|
if (signature === summarySignature) return
|
|
351
386
|
summarySignature = signature
|
|
387
|
+
clearSummaryEffects()
|
|
352
388
|
summaryRoot.replaceChildren()
|
|
353
389
|
if (warning.length) summaryRoot.append(buildSummaryGroup('warning', '等待处理的会话', warning))
|
|
354
390
|
if (ongoing.length) summaryRoot.append(buildSummaryGroup('ongoing', '运行中的会话', ongoing))
|
|
355
391
|
if (done.length) summaryRoot.append(buildSummaryGroup('done', '完成未查看', done))
|
|
392
|
+
preview.reanchor(summaryRoot)
|
|
356
393
|
}
|
|
357
394
|
|
|
358
395
|
function scheduleVectors() {
|
|
@@ -487,6 +524,8 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
487
524
|
}
|
|
488
525
|
syncSummary(root, next)
|
|
489
526
|
} else {
|
|
527
|
+
preview.stop()
|
|
528
|
+
clearSummaryEffects()
|
|
490
529
|
summaryRoot?.remove()
|
|
491
530
|
summaryRoot = undefined
|
|
492
531
|
summarySignature = ''
|
|
@@ -508,6 +547,8 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
508
547
|
}
|
|
509
548
|
|
|
510
549
|
function clear() {
|
|
550
|
+
preview.stop()
|
|
551
|
+
clearSummaryEffects()
|
|
511
552
|
lifelineAnimator.setTarget(undefined, undefined)
|
|
512
553
|
for (const node of document?.querySelectorAll?.('[data-prts-owned-projection], [data-prts-owned-facility], [data-prts-owned-facility-spine], [data-prts-owned-facility-vector], [data-prts-owned-facility-defs]') ?? []) node.remove()
|
|
513
554
|
for (const node of owned) {
|
|
@@ -525,7 +566,10 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
525
566
|
}
|
|
526
567
|
|
|
527
568
|
return {
|
|
528
|
-
start() {
|
|
569
|
+
start(preferences = {}) {
|
|
570
|
+
const keys = ['sessionPreview','previewOngoing','previewWarning','previewDone','previewDoubleClick']
|
|
571
|
+
if (keys.some(key => previewPreferences[key] !== preferences[key])) preview.stop()
|
|
572
|
+
previewPreferences = Object.fromEntries(keys.map(key => [key, preferences[key]]))
|
|
529
573
|
if (!document) return
|
|
530
574
|
lifelineAnimator.start()
|
|
531
575
|
ensureFacilityTextureDefs(document)
|
|
@@ -550,7 +594,9 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
550
594
|
if (observer || !window?.MutationObserver) return
|
|
551
595
|
const observationRoot = document.querySelector('[data-prts-region="sessions"]')
|
|
552
596
|
if (!observationRoot) return
|
|
553
|
-
observer = new window.MutationObserver(
|
|
597
|
+
observer = new window.MutationObserver(mutations => {
|
|
598
|
+
if (mutations.some(mutation => !mutation.target.closest?.('[data-prts-owned-session-summary]'))) queueScan()
|
|
599
|
+
})
|
|
554
600
|
observer.observe(observationRoot, {
|
|
555
601
|
subtree: true,
|
|
556
602
|
childList: true,
|