@ash-qw/dsh-theme-prts 2.1.0 → 2.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ash-qw/dsh-theme-prts",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Unofficial Arknights P.R.T.S.-inspired UI theme for DeepSeek Harness Web and the official desktop client",
@@ -18,6 +18,8 @@ export function createBackgroundStore({ window, document }) {
18
18
  if (url) root.style.setProperty(`--prts-background-${slot}`, `url("${url}")`)
19
19
  else root.style.removeProperty(`--prts-background-${slot}`)
20
20
  }
21
+ const ratio = Number(state.sidebar?.width) / Number(state.sidebar?.height)
22
+ document.documentElement.style.setProperty('--prts-sidebar-image-ratio', String(Number.isFinite(ratio) && ratio > 0 ? ratio : 1))
21
23
  for (const listener of listeners) listener(state)
22
24
  }
23
25
  const request = async data => {
@@ -52,6 +54,7 @@ export function createBackgroundStore({ window, document }) {
52
54
  catch (error) { await refresh().catch(() => {}); throw error }
53
55
  },
54
56
  dispose() {
57
+ document.documentElement.style.removeProperty('--prts-sidebar-image-ratio')
55
58
  disposed = true; abort?.abort(); events?.close(); listeners.clear(); window.removeEventListener('focus', focus)
56
59
  for (const slot of Object.keys(backgroundLabels)) {
57
60
  document.documentElement.removeAttribute(`data-prts-background-${slot}`)
@@ -16,6 +16,8 @@ function cssPixels(value) {
16
16
  export function createHostGeometryAdapter({ document, window }) {
17
17
  const root = document?.documentElement
18
18
  let frame
19
+ let sidebar
20
+ let previousSidebarWidth
19
21
  let resizeObserver
20
22
  let frameRequest
21
23
  let resizeSettleTimer
@@ -31,6 +33,10 @@ export function createHostGeometryAdapter({ document, window }) {
31
33
  if (root.style.getPropertyValue(TOP_INSET_PROPERTY) !== value) {
32
34
  root.style.setProperty(TOP_INSET_PROPERTY, value)
33
35
  }
36
+ if (sidebar?.isConnected) {
37
+ const width = cssPixels(sidebar.getBoundingClientRect().width)
38
+ if (root.style.getPropertyValue('--prts-sidebar-width') !== width) root.style.setProperty('--prts-sidebar-width', width)
39
+ }
34
40
  return top
35
41
  }
36
42
 
@@ -62,6 +68,7 @@ export function createHostGeometryAdapter({ document, window }) {
62
68
  function onGeometryResize(entries = []) {
63
69
  let shouldMeasure = false
64
70
  for (const entry of entries) {
71
+ if (entry.target === sidebar) { shouldMeasure = true; continue }
65
72
  if (entry.target !== frame) continue
66
73
  const height = Number(entry.contentRect?.height)
67
74
  if (!Number.isFinite(height) || height <= 0) {
@@ -91,6 +98,9 @@ export function createHostGeometryAdapter({ document, window }) {
91
98
  else root.style.removeProperty(TOP_INSET_PROPERTY)
92
99
  previousValue = undefined
93
100
  previousPriority = undefined
101
+ if (previousSidebarWidth) root.style.setProperty('--prts-sidebar-width', previousSidebarWidth)
102
+ else root.style.removeProperty('--prts-sidebar-width')
103
+ sidebar = undefined
94
104
  frame = undefined
95
105
  observedFrameHeight = undefined
96
106
  started = false
@@ -105,6 +115,8 @@ export function createHostGeometryAdapter({ document, window }) {
105
115
  }
106
116
  if (started) dispose()
107
117
  frame = nextFrame
118
+ sidebar = frame.querySelector?.('[data-prts-region="sessions"]')
119
+ previousSidebarWidth = root.style.getPropertyValue('--prts-sidebar-width')
108
120
  previousValue = root.style.getPropertyValue(TOP_INSET_PROPERTY)
109
121
  previousPriority = root.style.getPropertyPriority(TOP_INSET_PROPERTY)
110
122
  started = true
@@ -112,6 +124,7 @@ export function createHostGeometryAdapter({ document, window }) {
112
124
  if (typeof window?.ResizeObserver === 'function') {
113
125
  resizeObserver = new window.ResizeObserver(onGeometryResize)
114
126
  resizeObserver.observe(frame)
127
+ if (sidebar) resizeObserver.observe(sidebar)
115
128
  }
116
129
  window?.addEventListener?.('resize', onWindowResize, { passive: true })
117
130
  scheduleSettledMeasure()
@@ -2,7 +2,7 @@
2
2
  // See src/vendor/react-bits/LICENSE.md and THIRD_PARTY_NOTICES.md.
3
3
  export const BACKGROUND_OPACITY_CONTROLS = Object.freeze([
4
4
  { key: 'sidebarOpacity', label: '会话列表侧栏', property: '--prts-sidebar-opacity', value: 94, blurKey: 'sidebarBlur', blurProperty: '--prts-sidebar-blur', blurDefault: 8 },
5
- { key: 'railOpacity', label: '设置侧栏', property: '--prts-rail-opacity', value: 70, blurKey: 'railBlur', blurProperty: '--prts-rail-blur', blurDefault: 12, fullBackgroundOnly: true },
5
+ { key: 'railOpacity', label: '设置侧栏', property: '--prts-rail-opacity', value: 70, blurKey: 'railBlur', blurProperty: '--prts-rail-blur', blurDefault: 12 },
6
6
  { key: 'bakerOpacity', label: 'Baker 气泡', property: '--prts-baker-opacity', value: 50, blurKey: 'bakerBlur', blurProperty: '--prts-baker-blur', blurDefault: 8, theme: 'deck-chat' },
7
7
  { key: 'classicUserOpacity', label: '经典用户消息', property: '--prts-classic-user-opacity', value: 50, blurKey: 'classicUserBlur', blurProperty: '--prts-classic-user-blur', blurDefault: 8, theme: 'native' },
8
8
  ])
@@ -109,8 +109,7 @@ export function mountOpacityDials({ section, window, onChange }) {
109
109
  paint(value)
110
110
  return {
111
111
  update(preferences, active) {
112
- container.hidden = Boolean((definition.theme && preferences.conversationStyle !== definition.theme)
113
- || (definition.fullBackgroundOnly && preferences.backgroundMode !== 'full'))
112
+ container.hidden = Boolean(definition.theme && preferences.conversationStyle !== definition.theme)
114
113
  saved = 100 - Math.round((preferences[definition.key] ?? (100 - definition.value) / 100) * 100)
115
114
  if (!grip && section.ownerDocument.activeElement !== input) paint(saved)
116
115
  savedBlur = preferences[definition.blurKey] ?? definition.blurDefault
@@ -127,7 +126,8 @@ export function mountOpacityDials({ section, window, onChange }) {
127
126
  const split = preferences.backgroundMode === 'split'
128
127
  section.querySelector('[data-prts-opacity-settings]').hidden = preferences.backgroundMode === 'off'
129
128
  const conversationImage = full || (split && Boolean(state.conversation))
130
- const active = [full || (split && Boolean(state.sidebar)), full,
129
+ const sidebarImage = full || (split && Boolean(state.sidebar))
130
+ const active = [sidebarImage, sidebarImage,
131
131
  conversationImage && preferences.conversationStyle === 'deck-chat',
132
132
  conversationImage && preferences.conversationStyle === 'native']
133
133
  controls.forEach((control,i)=>control.update(preferences,active[i]))
@@ -1,3 +1,4 @@
1
+ import { SCHEME_COMMIT_PROGRESS } from './theme-controller-v2.js'
1
2
  import { createThemeSettingsOverlay, themeSettingsMarkup } from './theme-settings-workbench.js'
2
3
 
3
4
  const PHONE_RAIL_BREAKPOINT = 640
@@ -83,9 +84,9 @@ export function createOperationsShell({
83
84
  try { return window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true } catch { return false }
84
85
  }
85
86
 
86
- function phoneRailGesture() {
87
+ function phoneRailGesture(pointerType) {
87
88
  if (railMode !== 'overlay' || Number(window.innerWidth) > PHONE_RAIL_BREAKPOINT) return false
88
- return railPointerQuery?.matches === true
89
+ return pointerType === 'touch' || railPointerQuery?.matches === true
89
90
  || railHoverQuery?.matches === true
90
91
  || Number(window.navigator?.maxTouchPoints) > 0
91
92
  }
@@ -105,13 +106,16 @@ export function createOperationsShell({
105
106
 
106
107
  function armRailGesture() {
107
108
  railHoldTimer = undefined
108
- if (!railPointer || !phoneRailGesture()) return
109
+ if (!railPointer || !phoneRailGesture('touch')) return
109
110
  railPointer.armed = true
110
111
  railLauncher?.setAttribute('data-prts-rail-gesture-active', '')
111
112
  }
112
113
 
114
+ let railInputType
113
115
  function onRailPointerDown(event) {
114
- if (!phoneRailGesture() || railPointer || (event.button ?? 0) !== 0) return
116
+ railInputType = event.pointerType || 'mouse'
117
+ if (railInputType !== 'touch') return
118
+ if (!phoneRailGesture(event.pointerType) || railPointer || (event.button ?? 0) !== 0) return
115
119
  const pointerId = event.pointerId ?? 1
116
120
  railPointer = {
117
121
  pointerId,
@@ -159,7 +163,9 @@ export function createOperationsShell({
159
163
  }
160
164
 
161
165
  function onRailClick(event) {
162
- if (phoneRailGesture() && Number(event?.detail) > 0) {
166
+ const inputType = event?.pointerType || railInputType
167
+ railInputType = undefined
168
+ if (phoneRailGesture(inputType) && inputType === 'touch' && Number(event?.detail) > 0) {
163
169
  event.preventDefault?.()
164
170
  return
165
171
  }
@@ -262,9 +268,19 @@ export function createOperationsShell({
262
268
  }, 1000)
263
269
  }
264
270
 
271
+ function flushSchemeProgress(pointer) {
272
+ if (pointer.frame !== undefined) window.cancelAnimationFrame(pointer.frame)
273
+ pointer.frame = undefined
274
+ if (pointer.progress !== undefined) {
275
+ pointer.gesture?.update?.(pointer.progress)
276
+ pointer.progress = undefined
277
+ }
278
+ }
279
+
265
280
  function settleSchemePointer({ commit = false } = {}) {
266
281
  const pointer = schemePointer
267
282
  if (!pointer) return
283
+ flushSchemeProgress(pointer)
268
284
  schemePointer = undefined
269
285
  try { schemeToggle?.releasePointerCapture?.(pointer.pointerId) } catch {}
270
286
  if (!pointer.dragging) return
@@ -313,19 +329,25 @@ export function createOperationsShell({
313
329
  }
314
330
  }
315
331
  event.preventDefault?.()
316
- pointer.gesture?.update?.(schemePointerProgress(event))
332
+ pointer.progress = schemePointerProgress(event)
333
+ if (pointer.frame === undefined) pointer.frame = window.requestAnimationFrame(() => {
334
+ pointer.frame = undefined
335
+ if (schemePointer === pointer) flushSchemeProgress(pointer)
336
+ })
317
337
  }
318
338
 
319
339
  function onSchemePointerUp(event) {
320
340
  const pointer = schemePointer
321
341
  if (!pointer || (event.pointerId ?? 1) !== pointer.pointerId) return
322
- const commit = pointer.dragging && schemePointerProgress(event) >= .5
342
+ if (pointer.dragging) pointer.progress = schemePointerProgress(event)
343
+ const commit = pointer.dragging && schemePointerProgress(event) >= SCHEME_COMMIT_PROGRESS
323
344
  settleSchemePointer({ commit })
324
345
  }
325
346
 
326
347
  function cancelSchemePointer() {
327
348
  const pointer = schemePointer
328
349
  if (!pointer) return
350
+ if (pointer.frame !== undefined) window.cancelAnimationFrame(pointer.frame)
329
351
  if (pointer.dragging) {
330
352
  try { pointer.gesture?.cancel?.() } catch {}
331
353
  }
@@ -353,7 +375,7 @@ export function createOperationsShell({
353
375
  const overlay = railMode === 'overlay'
354
376
  const open = overlay && root.hasAttribute('data-prts-rail-open')
355
377
  railLauncher.setAttribute('aria-expanded', String(open))
356
- const closedLabel = phoneRailGesture() ? '长按并向下滑动打开 P.R.T.S. 导航' : '打开 P.R.T.S. 导航'
378
+ const closedLabel = phoneRailGesture() ? '打开 P.R.T.S. 导航(触摸时长按并下滑)' : '打开 P.R.T.S. 导航'
357
379
  railLauncher.setAttribute('aria-label', open ? '关闭 P.R.T.S. 导航' : closedLabel)
358
380
  if (overlay && !open) {
359
381
  navRail.setAttribute('aria-hidden', 'true')
@@ -418,12 +440,16 @@ export function createOperationsShell({
418
440
  }
419
441
 
420
442
  function onResponsiveChange() {
443
+ railInputType = undefined
421
444
  clearRailGesture()
422
445
  setRailViewport(window.innerWidth)
423
446
  syncRailState()
424
447
  }
425
448
 
449
+ let railPhoneQuery
426
450
  function bindResponsiveQueries() {
451
+ railPhoneQuery = window.matchMedia?.('(max-width: 640px)')
452
+ railPhoneQuery?.addEventListener?.('change', onResponsiveChange)
427
453
  railViewportQuery = window.matchMedia?.('(min-width: 1180px)')
428
454
  railPointerQuery = window.matchMedia?.('(pointer: coarse)')
429
455
  railHoverQuery = window.matchMedia?.('(hover: none)')
@@ -507,6 +533,9 @@ export function createOperationsShell({
507
533
  schemeToggle?.removeEventListener('lostpointercapture', onSchemeLostPointerCapture)
508
534
  window.removeEventListener?.('blur', cancelSchemePointer)
509
535
  themeDisable?.removeEventListener('click', disableTheme)
536
+ railPhoneQuery?.removeEventListener?.('change', onResponsiveChange)
537
+ railPhoneQuery = undefined
538
+ railInputType = undefined
510
539
  railViewportQuery?.removeEventListener?.('change', onResponsiveChange)
511
540
  railPointerQuery?.removeEventListener?.('change', onResponsiveChange)
512
541
  railHoverQuery?.removeEventListener?.('change', onResponsiveChange)
@@ -42,7 +42,9 @@ export const DEFAULT_PREFERENCES = Object.freeze({
42
42
  particlePattern: 'orthogonal',
43
43
  railDefaultHidden: false,
44
44
  sessionPreview: true,
45
+ aiToolsEnabled: true,
45
46
  previewOngoing: true,
47
+ previewTools: true,
46
48
  previewWarning: true,
47
49
  previewDone: true,
48
50
  previewFollowOngoing: true,
@@ -101,7 +103,8 @@ export const PREFERENCE_GROUPS = Object.freeze({
101
103
  particles: Object.freeze(['conversationParticleDensity', 'heroParticleDensity', 'particleTraversalSpeed']),
102
104
  material: Object.freeze(['glass', 'composerGlass']),
103
105
  accessibility: Object.freeze(['motion', 'bootAnimation', 'cursor']),
104
- navigation: Object.freeze(['railDefaultHidden', 'sessionPreview', 'previewOngoing', 'previewWarning', 'previewDone', 'previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning', 'previewReplyLimit']),
106
+ aiTools: Object.freeze(['aiToolsEnabled']),
107
+ navigation: Object.freeze(['railDefaultHidden', 'sessionPreview', 'previewOngoing', 'previewTools', 'previewWarning', 'previewDone', 'previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning', 'previewReplyLimit']),
105
108
  conversation: Object.freeze(['sessionFlow', 'sessionGlow', 'sessionFlowPalette', 'sessionFlowColorBack', 'sessionFlowColorCore', 'sessionFlowColorFront', 'sessionFlowSpeed', 'conversationStyle', 'bakerGroupAvatars', 'bakerLoading', 'conversationEmblem']),
106
109
  })
107
110
 
@@ -237,7 +240,9 @@ export function normalizePreferences(value) {
237
240
  sessionFlowColorFront: sessionFlowColors.front,
238
241
  sessionFlowSpeed: sessionFlowSpeedOr(input.sessionFlowSpeed, DEFAULT_PREFERENCES.sessionFlowSpeed),
239
242
  sessionPreview: booleanOr(input.sessionPreview, DEFAULT_PREFERENCES.sessionPreview),
243
+ aiToolsEnabled: booleanOr(input.aiToolsEnabled, DEFAULT_PREFERENCES.aiToolsEnabled),
240
244
  previewOngoing: booleanOr(input.previewOngoing, DEFAULT_PREFERENCES.previewOngoing),
245
+ previewTools: booleanOr(input.previewTools, DEFAULT_PREFERENCES.previewTools),
241
246
  previewWarning: booleanOr(input.previewWarning, DEFAULT_PREFERENCES.previewWarning),
242
247
  previewDone: booleanOr(input.previewDone, DEFAULT_PREFERENCES.previewDone),
243
248
  previewReplyLimit: [0, 5, 10, '0', '5', '10'].includes(input.previewReplyLimit) ? Number(input.previewReplyLimit) : 5,
@@ -39,6 +39,60 @@ export function latestAssistantReply(snapshot) {
39
39
  return ''
40
40
  }
41
41
 
42
+ // Call objects may be mutable on older hosts; compare raw arguments before reuse.
43
+ const toolSummaryCache = new WeakMap()
44
+ function toolSummary(call) {
45
+ const raw = call.argsRaw ?? call.call?.argsRaw ?? ''
46
+ const cached = toolSummaryCache.get(call)
47
+ if (cached?.raw === raw) return cached.text
48
+ let args
49
+ try { args = JSON.parse(raw) } catch {}
50
+ const summary = ['description', 'command', 'cmd', 'file_path', 'filePath', 'path', 'query', 'pattern', 'url']
51
+ .map(field => args?.[field]).find(value => typeof value === 'string' && value.trim())
52
+ const normalized = summary?.replace(/\s+/g, ' ').trim()
53
+ const text = normalized ? normalized.slice(0, 160) + (normalized.length > 160 ? '…' : '') : ''
54
+ toolSummaryCache.set(call, {raw, text})
55
+ return text
56
+ }
57
+
58
+ // Only calls after the latest settled prose belong to the current progress preview.
59
+ export function previewToolCalls(snapshot) {
60
+ const turn = snapshot?.timeline?.turnOrder?.at(-1)
61
+ const order = snapshot?.order ?? []
62
+ const calls = [], seen = new Set()
63
+ const visit = (call, key) => {
64
+ if (!call) return
65
+ const id = call.callId ?? key
66
+ if (!seen.has(id)) { seen.add(id); calls.push({call, key: id}) }
67
+ for (const [index, child] of (call.subCalls ?? []).entries()) visit(child, key + ':' + index)
68
+ }
69
+ for (let i = order.length - 1; i >= 0; i--) {
70
+ const node = snapshot.nodes?.get?.(order[i])
71
+ if (['user', 'steering', 'turn-trigger'].includes(node?.kind)) break
72
+ if (turn !== undefined && node?.location?.turn?.turn !== turn) continue
73
+ if (node?.visibility === 'hidden') continue
74
+ if (node?.kind === 'assistant-step' && node.data?.status === 'settled'
75
+ && node.data.blocks?.some(block => block.kind === 'text' && block.text?.trim())) break
76
+ if (node?.kind === 'tool-call') visit(node.data?.root, order[i])
77
+ }
78
+ return calls.sort((a, b) => (b.call.callTime ?? b.call.time ?? 0) - (a.call.callTime ?? a.call.time ?? 0)).map(({call, key}) => {
79
+ const text = toolSummary(call)
80
+ return {
81
+ key,
82
+ name: call.name || call.call?.name || '工具调用',
83
+ status: call.kind === 'tool-result' ? call.isError ? '失败' : '已完成'
84
+ : call.phase === 'preparing' ? '准备中' : '执行中',
85
+ summary: text ? text
86
+ : call.phase === 'preparing' ? '正在准备调用…' : '暂无操作摘要',
87
+ }
88
+ })
89
+ }
90
+
91
+ export function latestToolCall(snapshot) {
92
+ const records = previewToolCalls(snapshot)
93
+ return records.length ? {...records[0], records} : undefined
94
+ }
95
+
42
96
  export function completedPreviewReplies(snapshot) {
43
97
  const replies = []
44
98
  const latestTurn = snapshot?.timeline?.turnOrder?.at(-1)
@@ -142,8 +196,8 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
142
196
  active = undefined
143
197
  if (!old) return
144
198
  window.clearTimeout(old.leaveTimer)
145
- window.clearTimeout(old.delay)
146
- window.clearTimeout(old.refresh)
199
+ window.cancelAnimationFrame(old.delay)
200
+ window.cancelAnimationFrame(old.refresh)
147
201
  old.unsubscribe?.()
148
202
  old.unsubscribeStatus?.()
149
203
  if (keepWarm && old.reference && old.source) {
@@ -311,6 +365,39 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
311
365
  body.setAttribute('data-prts-preview-body', '')
312
366
  panel.querySelector('p').replaceWith(body)
313
367
  }
368
+ panel.querySelector('[data-prts-preview-tool]')?.remove()
369
+ const toolSection = panel.ownerDocument.createElement('div')
370
+ toolSection.setAttribute('data-prts-preview-tool', '')
371
+ toolSection.hidden = true
372
+ body.before(toolSection)
373
+ let previousTool, currentTool
374
+ const toolEntry = panel.ownerDocument.createElement('button')
375
+ toolEntry.type = 'button'
376
+ toolEntry.setAttribute('data-prts-tool-entry', '')
377
+ const toolHeading = panel.ownerDocument.createElement('small')
378
+ const toolName = panel.ownerDocument.createElement('span')
379
+ toolEntry.append(toolHeading, toolName)
380
+ toolSection.append(toolEntry)
381
+ const openToolDetail = (refresh = false) => {
382
+ if (currentTool) showDetail({key: 'latest-tool', tool: currentTool}, 0, toolEntry, refresh)
383
+ }
384
+ toolEntry.addEventListener('pointermove', () => openToolDetail())
385
+ toolEntry.addEventListener('focus', () => openToolDetail())
386
+ toolEntry.addEventListener('click', () => openToolDetail())
387
+ const renderTool = tool => {
388
+ const signature = JSON.stringify(tool)
389
+ if (signature === previousTool) return
390
+ previousTool = signature
391
+ currentTool = tool
392
+ toolSection.hidden = !tool
393
+ if (!tool) {
394
+ if (detailKey === 'latest-tool') hideDetail()
395
+ return
396
+ }
397
+ toolHeading.textContent = '工具 · ' + tool.status + ' · ' + tool.records.length + ' 次'
398
+ toolName.textContent = tool.name
399
+ if (detailKey === 'latest-tool') openToolDetail(true)
400
+ }
314
401
  const choices = panel.ownerDocument.createElement('div')
315
402
  choices.setAttribute('data-prts-preview-choices', '')
316
403
  panel.querySelector('[data-prts-preview-choices]')?.remove()
@@ -450,9 +537,9 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
450
537
  detailEntry?.removeAttribute('data-prts-reply-active')
451
538
  detailEntry = undefined
452
539
  }
453
- const showDetail = (reply, index, item) => {
540
+ const showDetail = (reply, index, item, refresh = false) => {
454
541
  keep()
455
- if (detailKey === reply.key && current.detail && !current.detail.hidden) return
542
+ if (!refresh && detailKey === reply.key && current.detail && !current.detail.hidden) return
456
543
  if (!current.detail) {
457
544
  const detail = panel.ownerDocument.createElement('section')
458
545
  detail.setAttribute('data-prts-summary-reply', '')
@@ -472,10 +559,36 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
472
559
  detailEntry?.removeAttribute('data-prts-reply-active')
473
560
  detailEntry = item
474
561
  item?.setAttribute('data-prts-reply-active', '')
475
- detail.querySelector('strong').textContent = '回复 ' + (reply.ordinal ?? index + 1)
562
+ detail.toggleAttribute('data-prts-tool-history', Boolean(reply.tool))
563
+ detail.querySelector('strong').textContent = reply.tool ? '本段工具记录 · ' + reply.tool.records.length + ' 次' : '回复 ' + (reply.ordinal ?? index + 1)
476
564
  const content = detail.querySelector('[data-prts-preview-body]')
477
- content.innerHTML = current.contentCache(reply.text).html
478
- content.scrollTop = 0
565
+ const scrollTop = refresh ? content.scrollTop : 0
566
+ let anchorKey, anchorOffset
567
+ if (refresh && scrollTop > 0) {
568
+ const anchor = [...content.children].find(row => row.getBoundingClientRect().bottom > content.getBoundingClientRect().top)
569
+ anchorKey = anchor?.dataset.toolKey
570
+ anchorOffset = anchor?.getBoundingClientRect().top
571
+ }
572
+ if (reply.tool) {
573
+ content.replaceChildren(...reply.tool.records.map(record => {
574
+ const row = panel.ownerDocument.createElement('article')
575
+ row.setAttribute('data-prts-tool-record', '')
576
+ row.dataset.toolKey = record.key
577
+ const heading = panel.ownerDocument.createElement('div')
578
+ const name = panel.ownerDocument.createElement('b')
579
+ name.textContent = record.name
580
+ const status = panel.ownerDocument.createElement('small')
581
+ status.textContent = record.status
582
+ heading.append(name, status)
583
+ const summary = panel.ownerDocument.createElement('p')
584
+ summary.textContent = record.summary
585
+ row.append(heading, summary)
586
+ return row
587
+ }))
588
+ } else content.innerHTML = current.contentCache(reply.text).html
589
+ content.scrollTop = scrollTop
590
+ const retainedAnchor = anchorKey && [...content.children].find(row => row.dataset.toolKey === anchorKey)
591
+ if (retainedAnchor) content.scrollTop += retainedAnchor.getBoundingClientRect().top - anchorOffset
479
592
  detail.hidden = false
480
593
  const rect = panel.getBoundingClientRect()
481
594
  const width = Math.min(340, window.innerWidth - 16)
@@ -548,11 +661,18 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
548
661
  }))
549
662
  }
550
663
  choices.hidden = !labels.length
664
+ const tool = state === 'ongoing' && !question && getPreviewPreferences?.().previewTools !== false ? latestToolCall(source?.getSnapshot()) : undefined
665
+ renderTool(tool)
666
+ if (tool) label.textContent = '运行中 · 最新活动'
551
667
  panel.setAttribute('aria-label', title + ':' + label.textContent)
552
668
  if (state === 'ongoing' && source) {
553
669
  const replies = selectReplies(source.getSnapshot(), replyLimit())
554
- if (replies === historyKeys) return
555
- panel.toggleAttribute('data-prts-preview-empty', !replies.length)
670
+ panel.toggleAttribute('data-prts-preview-empty', !replies.length && !tool)
671
+ if (replies === historyKeys) {
672
+ const emptyText = tool ? '' : '正在处理…'
673
+ if (!replies.length && body.textContent !== emptyText) body.textContent = emptyText
674
+ return
675
+ }
556
676
  const previousTop = body.scrollTop
557
677
  const scrollAnchor = previousTop > 0 ? [...body.children].find(entry => entry.getBoundingClientRect().bottom > body.getBoundingClientRect().top) : undefined
558
678
  const anchorTop = scrollAnchor?.getBoundingClientRect().top
@@ -568,7 +688,7 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
568
688
  }
569
689
  }
570
690
  if (!historyKeys.length) body.replaceChildren()
571
- if (!replies.length) body.textContent = '正在处理,等待本轮正文输出完成…'
691
+ if (!replies.length) body.textContent = tool ? '' : '正在处理…'
572
692
  for (let index = 0; index < replies.length; index++) {
573
693
  const reply = replies[index]
574
694
  if (retained.has(reply.key)) continue
@@ -580,6 +700,13 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
580
700
  item.setAttribute('data-prts-reply-entry', '')
581
701
  const heading = panel.ownerDocument.createElement('small')
582
702
  heading.textContent = '回复 ' + reply.ordinal
703
+ const icon = panel.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'svg')
704
+ icon.setAttribute('viewBox', '0 0 16 16')
705
+ icon.setAttribute('aria-hidden', 'true')
706
+ const outline = panel.ownerDocument.createElementNS(icon.namespaceURI, 'path')
707
+ outline.setAttribute('d', 'M3 2.5h10a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7l-4 2v-2a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1Z')
708
+ icon.append(outline)
709
+ heading.prepend(icon)
583
710
  const summary = panel.ownerDocument.createElement('span')
584
711
  summary.textContent = current.contentCache(reply.text).summary
585
712
  item.append(heading, summary)
@@ -610,7 +737,7 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
610
737
  ? approval
611
738
  ? [pending.toolName, pending.displayReason?.['zh-CN'] || pending.displayReason?.zh || pending.reason || pending.displayReason?.en || '请进入会话查看操作详情'].filter(Boolean).join(':')
612
739
  : first?.question || pendingQuestionPreview(pending)
613
- : source ? excerpt || (state === 'ongoing' ? '正在处理,尚未输出正文…' : '暂无正文回复,可进入会话查看执行记录。') : '正在读取…'
740
+ : source ? excerpt || (state === 'ongoing' ? '正在处理…' : '暂无正文回复,可进入会话查看执行记录。') : '正在读取…'
614
741
  if (previousText !== text || previousQuestion !== question) {
615
742
  const extendsReply = previousQuestion === question && previousText && text.startsWith(previousText)
616
743
  if (question) body.textContent = text
@@ -631,7 +758,8 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
631
758
  })
632
759
  render()
633
760
  body.scrollTop = 0
634
- current.delay = window.setTimeout(async () => {
761
+ current.delay = window.requestAnimationFrame(async () => {
762
+ current.delay = undefined
635
763
  try {
636
764
  const service = getUiConversation?.()
637
765
  const controller = getSessions?.() ?? sessions
@@ -643,11 +771,13 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
643
771
  current.source = source
644
772
  current.unsubscribe = source.subscribe(() => {
645
773
  if (active !== current || current.refresh !== undefined) return
646
- current.refresh = window.setTimeout(() => {
774
+ current.refresh = window.requestAnimationFrame(() => {
647
775
  current.refresh = undefined
648
- if (current.state === 'ongoing' && !current.editor && selectReplies(source.getSnapshot(), replyLimit()) === historyKeys) return
776
+ if (current.state === 'ongoing' && !current.editor
777
+ && selectReplies(source.getSnapshot(), replyLimit()) === historyKeys
778
+ && JSON.stringify(getPreviewPreferences?.().previewTools === false ? undefined : latestToolCall(source.getSnapshot())) === previousTool) return
649
779
  render()
650
- }, 120)
780
+ })
651
781
  })
652
782
  render()
653
783
  } catch {
@@ -659,7 +789,7 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
659
789
  render()
660
790
  if (body.textContent.trim() === '正在读取…') body.textContent = '暂时无法读取回复,可点击会话查看'
661
791
  }
662
- }, cached ? 0 : 80)
792
+ })
663
793
  }
664
794
  function leave() {
665
795
  if (!active || active.sticky) return
@@ -673,6 +803,8 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
673
803
  if (!active) return
674
804
  const anchor = [...root.querySelectorAll('[data-prts-session-summary-item]')]
675
805
  .find(item => item.getAttribute('data-prts-session-summary-item') === active.id)
806
+ // A read completion (or removed session) has no status row to own a preview.
807
+ if (!anchor) { stop(); return }
676
808
  active.group?.removeAttribute('data-prts-preview-open')
677
809
  active.anchor?.removeAttribute('data-prts-preview-active')
678
810
  active.anchor = anchor
@@ -976,7 +976,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
976
976
 
977
977
  return {
978
978
  start(preferences = {}) {
979
- const keys = ['sessionPreview','previewOngoing','previewWarning','previewDone','previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning', 'previewReplyLimit']
979
+ const keys = ['sessionPreview','previewOngoing','previewTools','previewWarning','previewDone','previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning', 'previewReplyLimit']
980
980
  if (keys.some(key => previewPreferences[key] !== preferences[key])) preview.stop()
981
981
  previewPreferences = Object.fromEntries(keys.map(key => [key, preferences[key]]))
982
982
  if (!document) return
@@ -1,6 +1,8 @@
1
1
  import { createCursorAdapter } from './cursor-adapter.js'
2
2
  import { normalizePreferences } from './preferences.js'
3
3
 
4
+ export const SCHEME_COMMIT_PROGRESS = .15
5
+
4
6
  const ROOT_ATTRIBUTES = [
5
7
  'data-dsh-prts',
6
8
  'data-prts-scheme',
@@ -244,11 +246,23 @@ export function createThemeController({ document, window, cssText, service, curs
244
246
  function updateRevealHud(session) {
245
247
  if (!session || session.settled) return
246
248
  const percentage = Math.round(revealProgress(session.progress) * 100)
247
- if (session.hudEdge) session.hudEdge.dataset.prtsSchemeRevealSide = 'left'
248
- root.style.setProperty('--prts-scheme-control-progress', String(session.progress))
249
- root.style.setProperty('--prts-scheme-control-meter-height', `${revealProgress(session.progress) * 48}px`)
250
- root.style.setProperty('--prts-scheme-control-value-image', revealValueImageUrl(percentage))
251
- root.toggleAttribute(TRANSITION_ARMED_ATTRIBUTE, session.interactive && session.progress >= .5)
249
+ if (session.hudEdge && session.hudEdge.dataset.prtsSchemeRevealSide !== 'left') session.hudEdge.dataset.prtsSchemeRevealSide = 'left'
250
+ if (session.hudPercentage !== percentage) {
251
+ session.hudPercentage = percentage
252
+ const image = revealValueImageUrl(percentage)
253
+ if (session.hudAnimation?.effect?.setKeyframes) {
254
+ const frame = {
255
+ backgroundImage: image + ',linear-gradient(#ffd400,#ffd400)',
256
+ backgroundSize: '24px 11px,2px ' + (percentage * .48) + 'px',
257
+ }
258
+ session.hudAnimation.effect.setKeyframes([frame, frame])
259
+ } else {
260
+ root.style.setProperty('--prts-scheme-control-meter-height', `${percentage * .48}px`)
261
+ root.style.setProperty('--prts-scheme-control-value-image', image)
262
+ }
263
+ }
264
+ const armed = session.interactive && session.progress >= SCHEME_COMMIT_PROGRESS
265
+ if (root.hasAttribute(TRANSITION_ARMED_ATTRIBUTE) !== armed) root.toggleAttribute(TRANSITION_ARMED_ATTRIBUTE, armed)
252
266
  }
253
267
 
254
268
  function releaseRevealHud(session) {
@@ -361,11 +375,16 @@ export function createThemeController({ document, window, cssText, service, curs
361
375
  ], options, '::view-transition-new(prts-scheme-grid)')
362
376
  const mark = revealNamedAnimation([
363
377
  { offset: 0, translate: revealGroupTranslate(0), rotate: '45deg' },
364
- { offset: .49, translate: revealGroupTranslate(session.geometry.width * .49), rotate: '45deg' },
365
- { offset: .51, translate: revealGroupTranslate(session.geometry.width * .51), rotate: '225deg' },
378
+ { offset: SCHEME_COMMIT_PROGRESS - .01, translate: revealGroupTranslate(session.geometry.width * (SCHEME_COMMIT_PROGRESS - .01)), rotate: '45deg' },
379
+ { offset: SCHEME_COMMIT_PROGRESS + .01, translate: revealGroupTranslate(session.geometry.width * (SCHEME_COMMIT_PROGRESS + .01)), rotate: '225deg' },
366
380
  { offset: 1, translate: revealGroupTranslate(session.geometry.width), rotate: '225deg' },
367
381
  ], options, '::view-transition-group(prts-scheme-mark)')
368
382
  session.scrubAnimations = [clip, edge, node, grid, gridPhase, mark]
383
+ session.hudAnimation = revealNamedAnimation([{}], options, '::view-transition-new(prts-scheme-node)')
384
+ session.hudAnimation.pause?.()
385
+ session.hudAnimation.currentTime = 0
386
+ session.hudPercentage = undefined
387
+ updateRevealHud(session)
369
388
  for (const animation of session.scrubAnimations) {
370
389
  animation.pause?.()
371
390
  animation.currentTime = session.progress * session.duration
@@ -377,11 +396,10 @@ export function createThemeController({ document, window, cssText, service, curs
377
396
  if (!session || session.settled) return
378
397
  session.progress = revealProgress(value)
379
398
  session.revealX = session.progress * session.geometry.width
380
- root.style.setProperty('--prts-scheme-reveal-x', `${session.revealX}px`)
399
+ if (!session.scrubAnimations) root.style.setProperty('--prts-scheme-reveal-x', `${session.revealX}px`)
381
400
  if (session.scrubAnimations) {
382
401
  try {
383
402
  for (const animation of session.scrubAnimations) {
384
- animation.pause?.()
385
403
  animation.currentTime = session.progress * session.duration
386
404
  }
387
405
  } catch {}
@@ -402,12 +420,19 @@ export function createThemeController({ document, window, cssText, service, curs
402
420
  for (const animation of session?.scrubAnimations || []) {
403
421
  try { animation?.cancel?.() } catch {}
404
422
  }
405
- if (session) session.scrubAnimations = undefined
423
+ session?.hudAnimation?.cancel?.()
424
+ if (session) {
425
+ session.scrubAnimations = undefined
426
+ session.hudAnimation = undefined
427
+ session.hudPercentage = undefined
428
+ }
406
429
  }
407
430
 
408
431
  function animateRevealTo(session, value, duration, easing) {
409
432
  if (!session || session.settled) return Promise.resolve(false)
410
433
  cancelRevealAnimation(session)
434
+ // Transfer the last HUD value once before the release animation takes over.
435
+ updateRevealHud(session)
411
436
  const targetProgress = revealProgress(value)
412
437
  const targetX = revealTargetX(session, targetProgress)
413
438
  if (!(duration > 0)) {
@@ -449,8 +474,8 @@ export function createThemeController({ document, window, cssText, service, curs
449
474
  { backgroundSize: `2px ${targetProgress * 48}px` },
450
475
  ], options, '::view-transition-new(prts-scheme-node)')
451
476
  const mark = revealNamedAnimation([
452
- { translate: revealGroupTranslate(session.revealX), rotate: session.progress >= .5 ? '225deg' : '45deg' },
453
- { translate: revealGroupTranslate(targetX), rotate: targetProgress >= .5 ? '225deg' : '45deg' },
477
+ { translate: revealGroupTranslate(session.revealX), rotate: session.progress >= SCHEME_COMMIT_PROGRESS ? '225deg' : '45deg' },
478
+ { translate: revealGroupTranslate(targetX), rotate: targetProgress >= SCHEME_COMMIT_PROGRESS ? '225deg' : '45deg' },
454
479
  ], options, '::view-transition-group(prts-scheme-mark)')
455
480
  animations = [clip, edge, node, grid, gridPhase, nodeProgress, mark]
456
481
  } catch {
@@ -608,7 +633,7 @@ export function createThemeController({ document, window, cssText, service, curs
608
633
  let settled = false
609
634
  return {
610
635
  update(value) { if (!settled) progress = revealProgress(value) },
611
- finish(commit = progress >= .5) {
636
+ finish(commit = progress >= SCHEME_COMMIT_PROGRESS) {
612
637
  if (settled) return root.dataset.prtsScheme
613
638
  settled = true
614
639
  return commit ? setTheme(target, { animate: false }) : source