@ash-qw/dsh-theme-prts 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ash-qw/dsh-theme-prts",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
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",
@@ -103,6 +103,52 @@ function cssUrl(value) {
103
103
 
104
104
  export function createAssistantGlassAdapter({ document, window, avatarImage = '' }) {
105
105
  let observer
106
+ let observationRoot
107
+ let groupAvatars = true
108
+ let avatarsDirty = true
109
+ const avatarSteps = new Set()
110
+ const emptySteps = new Set()
111
+ function refreshAvatars() {
112
+ if (!avatarsDirty || !observationRoot) return
113
+ avatarsDirty = false
114
+ for (const step of avatarSteps) {
115
+ if (!observationRoot.contains(step)) {
116
+ step.removeAttribute('data-prts-avatar-hidden')
117
+ avatarSteps.delete(step)
118
+ }
119
+ }
120
+ emptySteps.clear()
121
+ if (!groupAvatars) {
122
+ for (const step of avatarSteps) step.removeAttribute('data-prts-avatar-hidden')
123
+ avatarSteps.clear()
124
+ return
125
+ }
126
+ const groups = new Map()
127
+ for (const node of observationRoot.querySelectorAll('[data-chat-flow-kind="user"], [data-chat-flow-kind="steering"], ' + STEP_SELECTOR)) {
128
+ const scope = node.closest('[data-conversation-scroll]') ?? observationRoot
129
+ let group = groups.get(scope)
130
+ if (!group) groups.set(scope, group = new Set())
131
+ if (node.getAttribute('data-chat-flow-kind') !== 'assistant-step') {
132
+ if (node.closest('[hidden], [data-turn-process-hidden="true"]')) continue
133
+ group.clear()
134
+ continue
135
+ }
136
+ const surface = stepSurfaces.get(node) ?? resolveAssistantSurface(node)
137
+ const hasContent = Boolean(surface?.textContent?.trim() || surface?.querySelector('img,video,audio,canvas,svg'))
138
+ if (!hasContent) emptySteps.add(node)
139
+ const eligible = hasContent && node.getAttribute('data-chat-group-part') !== 'reasoning'
140
+ && !node.closest('[hidden], [data-turn-process-hidden="true"]')
141
+ const turn = node.getAttribute('data-chat-turn') ?? 'unassigned'
142
+ // The compact-answer flag disappears when expanded; process membership
143
+ // remains stable, so final responses keep their independent avatars.
144
+ const process = node.hasAttribute('data-turn-process-member')
145
+ const show = eligible && (!process || !group.has(turn))
146
+ if (show && process) group.add(turn)
147
+ if (show) node.removeAttribute('data-prts-avatar-hidden')
148
+ else if (!node.hasAttribute('data-prts-avatar-hidden')) node.setAttribute('data-prts-avatar-hidden', '')
149
+ avatarSteps.add(node)
150
+ }
151
+ }
106
152
  let scanFrame
107
153
  const ownedSurfaces = new Set()
108
154
  const ownedSteps = new Set()
@@ -198,6 +244,7 @@ export function createAssistantGlassAdapter({ document, window, avatarImage = ''
198
244
  }
199
245
 
200
246
  function scan() {
247
+ refreshAvatars()
201
248
  const startedAt = window?.performance?.now?.() ?? Date.now()
202
249
  let processed = 0
203
250
  for (const step of pendingSteps) {
@@ -224,10 +271,16 @@ export function createAssistantGlassAdapter({ document, window, avatarImage = ''
224
271
 
225
272
  function onMutations(mutations) {
226
273
  for (const mutation of mutations) {
274
+ if (mutation.type === 'characterData') {
275
+ const step = mutation.target.parentElement?.closest(STEP_SELECTOR)
276
+ if (emptySteps.has(step)) avatarsDirty = true
277
+ continue
278
+ }
227
279
  const parentStep = mutation.target?.closest?.(STEP_SELECTOR)
228
280
  const stableContentMutation = parentStep
229
281
  && stableBodyContains(parentStep, mutation.target)
230
282
  && !(mutation.type === 'attributes' && stepSurfaces.get(parentStep) === mutation.target)
283
+ if (!stableContentMutation || emptySteps.has(parentStep)) avatarsDirty = true
231
284
  if (parentStep && !stableContentMutation) {
232
285
  pendingSteps.add(parentStep)
233
286
  }
@@ -239,7 +292,7 @@ export function createAssistantGlassAdapter({ document, window, avatarImage = ''
239
292
  if (surface && (surface === node || node.contains?.(surface))) pendingSteps.add(parentStep)
240
293
  }
241
294
  }
242
- if (pendingSteps.size) queueScan()
295
+ if (pendingSteps.size || avatarsDirty) queueScan()
243
296
  }
244
297
 
245
298
  function removeMarkers() {
@@ -248,13 +301,19 @@ export function createAssistantGlassAdapter({ document, window, avatarImage = ''
248
301
  }
249
302
 
250
303
  return {
251
- start() {
304
+ start({ groupAvatars: enabled = true } = {}) {
252
305
  if (!document) return
306
+ if (groupAvatars !== enabled) {
307
+ groupAvatars = enabled
308
+ avatarsDirty = true
309
+ refreshAvatars()
310
+ }
253
311
  mountAvatarImage()
254
- const observationRoot = document.querySelector('[data-prts-region="operation"]')
312
+ observationRoot = document.querySelector('[data-prts-region="operation"]')
255
313
  ?? document.querySelector('[data-slot="main.conversation"], [data-slot="conversation"]')
256
314
  if (!observationRoot) return
257
315
  if (observer) return
316
+ avatarsDirty = true
258
317
  collectInitialSteps(observationRoot)
259
318
  scan()
260
319
  const Observer = window?.MutationObserver
@@ -263,8 +322,9 @@ export function createAssistantGlassAdapter({ document, window, avatarImage = ''
263
322
  observer.observe(observationRoot, {
264
323
  subtree: true,
265
324
  childList: true,
325
+ characterData: true,
266
326
  attributes: true,
267
- attributeFilter: ['class', 'data-chat-flow-kind', 'data-chat-group-part', 'data-markdown', 'data-slot'],
327
+ attributeFilter: ['class', 'data-chat-flow-kind', 'data-chat-group-part', 'data-markdown', 'data-slot', 'data-chat-turn', 'hidden', 'data-turn-process-hidden', 'data-turn-process-member', 'data-turn-process-answer'],
268
328
  })
269
329
  },
270
330
  dispose() {
@@ -276,6 +336,10 @@ export function createAssistantGlassAdapter({ document, window, avatarImage = ''
276
336
  }
277
337
  scanFrame = undefined
278
338
  removeMarkers()
339
+ for (const step of avatarSteps) step.removeAttribute('data-prts-avatar-hidden')
340
+ avatarSteps.clear()
341
+ emptySteps.clear()
342
+ observationRoot = undefined
279
343
  releaseAvatarImage()
280
344
  },
281
345
  }
@@ -14,6 +14,8 @@ function closestWithClassSuffix(node, suffix) {
14
14
  function classify(button, operation) {
15
15
  // The host's process disclosure owns its transparent, full-width hit area.
16
16
  if (button.hasAttribute('data-turn-process')) return null
17
+ // Changed-file cards own one outer surface; their header and rows stay native.
18
+ if (button.closest('[data-changed-files]')) return null
17
19
  // File previews are transparent hit targets; the enclosing card owns its material and outline.
18
20
  if (button.closest('[data-presented-file]') && hasClassSuffix(button, '_cardPreview')) return null
19
21
  if (button.tagName !== 'BUTTON' || !operation.contains(button)) return null
@@ -88,8 +88,18 @@ export function createCursorAdapter({ document, window, assets }) {
88
88
  let originalCursor
89
89
  let originalPriority
90
90
  let appliedCursor
91
+ let appliedPriority
92
+ function ownsHandCursor() {
93
+ return handTarget?.style.cursor === appliedCursor
94
+ && handTarget.style.getPropertyPriority('cursor') === appliedPriority
95
+ }
96
+ function stopIfHandChanged() {
97
+ if (!handTarget || ownsHandCursor()) return false
98
+ reset()
99
+ return true
100
+ }
91
101
  function restoreHandTarget() {
92
- if (handTarget && handTarget.style.cursor === appliedCursor) {
102
+ if (handTarget && ownsHandCursor()) {
93
103
  if (originalCursor) handTarget.style.setProperty('cursor', originalCursor, originalPriority)
94
104
  else handTarget.style.removeProperty('cursor')
95
105
  }
@@ -103,6 +113,7 @@ export function createCursorAdapter({ document, window, assets }) {
103
113
  return target && target.getElementsByTagName('*').length <= maxAnimatedDescendants
104
114
  }
105
115
  function followHand(target) {
116
+ if (stopIfHandChanged()) return
106
117
  if (target === handTarget && target !== textTarget) return
107
118
  restoreHandTarget()
108
119
  if (!canAnimate(target) || !handCursor || target === textTarget) return
@@ -111,13 +122,16 @@ export function createCursorAdapter({ document, window, assets }) {
111
122
  handTarget = target
112
123
  originalCursor = target.style.getPropertyValue('cursor')
113
124
  originalPriority = target.style.getPropertyPriority('cursor')
125
+ appliedCursor = originalCursor
126
+ appliedPriority = originalPriority
114
127
  target.setAttribute('data-prts-hand-active', '')
115
128
  applyHandFrame()
116
129
  }
117
130
  function applyHandFrame() {
118
- if (!handTarget) return
131
+ if (stopIfHandChanged() || !handTarget) return
119
132
  handTarget.style.setProperty('cursor', handCursor)
120
133
  appliedCursor = handTarget.style.cursor
134
+ appliedPriority = handTarget.style.getPropertyPriority('cursor')
121
135
  }
122
136
  function clearTimer() {
123
137
  if (timer !== undefined) window.clearTimeout(timer)
@@ -144,10 +158,12 @@ export function createCursorAdapter({ document, window, assets }) {
144
158
  let index = 0
145
159
  function next() {
146
160
  timer = undefined
161
+ if (stopIfHandChanged()) return
147
162
  // The source ANI ends with a 60-second hold frame. Use the static state instead.
148
163
  if (index >= frames.length - 1) { done(); return }
149
164
  const frame = frames[index++]
150
165
  show(frame.url)
166
+ if (!handCursor) return
151
167
  timer = window.setTimeout(next, frame.duration)
152
168
  }
153
169
  next()
@@ -179,6 +195,7 @@ export function createCursorAdapter({ document, window, assets }) {
179
195
  play(gestureAssets.release, reset)
180
196
  }
181
197
  function up(event) {
198
+ if (stopIfHandChanged()) return
182
199
  if (pointerId === undefined || event.pointerId !== pointerId || event.button !== 0) return
183
200
  pointerId = undefined
184
201
  if (reduced()) { reset(); return }
@@ -245,7 +245,7 @@ export function customEmblemMarkup() {
245
245
  return `<section data-prts-custom-emblem>
246
246
  <header><strong>徽记管理</strong><small>新会话与正文分别设置,可在任意会话中让 AI 调整,设置在当前 DSH 实例共享</small></header>
247
247
  <div data-prts-emblem-hero-panel><h4>新会话徽记</h4><div data-prts-emblem-hero-row></div></div>
248
- <div><h4>正文轮换徽记</h4><small>按列表顺序随正文滚动轮换,可混合使用内置与上传的图片,最多 12 张。长按徽记选项任意位置拖动排序,松开保存。</small>
248
+ <div><h4>正文轮换徽记</h4><small data-prts-emblem-baker-note hidden>Baker 会话正文不显示或运行粒子徽记;下列配置保留供经典主题使用,新会话徽记不受影响。</small><small>按列表顺序随正文滚动轮换,可混合使用内置与上传的图片,最多 12 张。长按徽记选项任意位置拖动排序,松开保存。</small>
249
249
  <ol data-prts-emblem-list></ol>
250
250
  <div data-prts-emblem-actions><button type="button" data-prts-emblem-add>新增徽记</button><button type="button" data-prts-emblem-reset-list>恢复内置列表</button></div>
251
251
  </div>
@@ -252,7 +252,7 @@ export function applyPrtsPlugin(ctx, environment) {
252
252
  }
253
253
  particleField.update(preferences)
254
254
  composerGlass.start()
255
- if (preferences.conversationStyle === 'deck-chat') assistantGlass.start()
255
+ if (preferences.conversationStyle === 'deck-chat') assistantGlass.start({ groupAvatars: preferences.bakerGroupAvatars })
256
256
  else assistantGlass.dispose()
257
257
  toBottom.start()
258
258
  conversationControls.start()
@@ -431,7 +431,8 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
431
431
  : preferences.conversationParticleDensity
432
432
  const densityProfile = density => PARTICLE_DENSITY_PROFILES[density] ?? PARTICLE_DENSITY_PROFILES.standard
433
433
  const particleRadius = density => densityProfile(density ?? activeDensity()).radius
434
- const canRenderFrame = () => !disposed && documentVisible && canvasVisible && !hydrationPaused && !startupPaused && !schemeTransitionPaused && !resizing
434
+ const fieldEnabled = () => heroActive || (preferences.conversationStyle !== 'deck-chat' && preferences.conversationEmblem !== false)
435
+ const canRenderFrame = () => fieldEnabled() && !disposed && documentVisible && canvasVisible && !hydrationPaused && !startupPaused && !schemeTransitionPaused && !resizing
435
436
  const layoutWidth = () => width
436
437
  const layoutHeight = () => height
437
438
  const layoutShiftX = () => 0
@@ -496,7 +497,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
496
497
  }
497
498
 
498
499
  function populationSource(density = activeDensity()) {
499
- return heroActive ? targetsForHero(density) : targetsForSegment(segment, density)
500
+ return !fieldEnabled() ? [] : heroActive ? targetsForHero(density) : targetsForSegment(segment, density)
500
501
  }
501
502
 
502
503
  function targetParticleCount(density = activeDensity()) {
@@ -697,7 +698,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
697
698
 
698
699
  function scheduleTargetPrewarm() {
699
700
  cancelTargetPrewarm()
700
- if (typeof window.requestIdleCallback !== 'function' || !particles.length) return
701
+ if (preferences.conversationEmblem === false || preferences.conversationStyle === 'deck-chat' || !fieldEnabled() || typeof window.requestIdleCallback !== 'function' || !particles.length) return
701
702
  targetPrewarmQueue = (customEmblemConfiguration?.conversation ?? emblemSequence).map((_, index) => index)
702
703
  .filter(index => index !== sequenceIndexForSegment(segment))
703
704
  const warmNext = () => {
@@ -718,6 +719,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
718
719
  }
719
720
 
720
721
  function buildParticles() {
722
+ if (!fieldEnabled()) { particles = []; cancelTargetPrewarm(); return false }
721
723
  if (startupPaused) return false
722
724
  const selected = populationSource()
723
725
  if (!selected.length) {
@@ -796,6 +798,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
796
798
  }
797
799
 
798
800
  function startDensityReassembly(nextCount, force = false) {
801
+ if (!fieldEnabled()) return
799
802
  const baseNextTargets = populationSource()
800
803
  const targetCount = baseNextTargets.length
801
804
  if (!targetCount) {
@@ -921,6 +924,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
921
924
  }
922
925
 
923
926
  function commitTransitionTargets(targetSegment, snap = false, density = preferences.conversationParticleDensity) {
927
+ if (!fieldEnabled()) return
924
928
  const selected = targetsForSegment(targetSegment, density)
925
929
  if (!selected.length) return
926
930
  segment = targetSegment
@@ -1507,7 +1511,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1507
1511
  heroActive = true
1508
1512
  scroller?.setAttribute('data-prts-hero-active', '')
1509
1513
  resetTraversal()
1510
- scheduleFrame()
1514
+ updateFrameGate()
1511
1515
  }
1512
1516
 
1513
1517
  function leaveHero() {
@@ -1516,6 +1520,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1516
1520
  heroMarker = undefined
1517
1521
  heroActive = false
1518
1522
  scroller?.removeAttribute('data-prts-hero-active')
1523
+ if (!fieldEnabled()) { resetTraversal(); updateFrameGate(); return }
1519
1524
  side = -1
1520
1525
  segment = 0
1521
1526
  traversalOffset = -currentScrollTop() * particleTraversalSpeed() / traversalViewport()
@@ -1558,7 +1563,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1558
1563
  if (heroActive) leaveHero()
1559
1564
  }
1560
1565
  function onScroll() {
1561
- if (!scroller || phoneStatic()) return
1566
+ if (!fieldEnabled() || !scroller || phoneStatic()) return
1562
1567
  if (heroActive) {
1563
1568
  scheduleFrame()
1564
1569
  return
@@ -1570,7 +1575,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1570
1575
  }
1571
1576
 
1572
1577
  function onPointerMove(event) {
1573
- if (!operation || transition || motionReduced() || phoneStatic()) return
1578
+ if (!fieldEnabled() || !operation || transition || motionReduced() || phoneStatic()) return
1574
1579
  pointerClientX = event.clientX
1575
1580
  pointerClientY = event.clientY
1576
1581
  pointerActive = true
@@ -1607,7 +1612,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1607
1612
  }
1608
1613
 
1609
1614
  function registerContentMutations(mutations) {
1610
- if (phoneStatic()) return
1615
+ if (!fieldEnabled() || phoneStatic()) return
1611
1616
  let weight = mutations.length
1612
1617
  for (const mutation of mutations) {
1613
1618
  for (const node of [...mutation.addedNodes, ...mutation.removedNodes]) {
@@ -1652,6 +1657,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1652
1657
  }
1653
1658
 
1654
1659
  function updateFrameGate() {
1660
+ if (canvas) canvas.hidden = !fieldEnabled()
1655
1661
  const now = Number(window.performance?.now?.()) || 0
1656
1662
  if (!canRenderFrame()) {
1657
1663
  pauseStartedAt ??= now
@@ -1741,7 +1747,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1741
1747
  if (!phoneStatic()) restoreTraversalFromScroll()
1742
1748
  commitTransitionTargets(segment, true)
1743
1749
  }
1744
- scheduleFrame()
1750
+ updateFrameGate()
1745
1751
  }
1746
1752
 
1747
1753
  function mount(nextOperation, nextScroller) {
@@ -1768,6 +1774,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1768
1774
  targetCache.clear()
1769
1775
  initializeFieldSize()
1770
1776
  syncHeroState()
1777
+ canvas.hidden = !fieldEnabled()
1771
1778
  if (!heroActive) restoreTraversalFromScroll()
1772
1779
  if (!particles.length && maskRecords.length) buildParticles()
1773
1780
  if (context && particles.length) {
@@ -1880,6 +1887,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1880
1887
  }
1881
1888
 
1882
1889
  findMount()
1890
+ if (!fieldEnabled()) resetTraversal()
1883
1891
  const previousActiveDensity = heroActive
1884
1892
  ? previousPreferences.heroParticleDensity
1885
1893
  : previousPreferences.conversationParticleDensity
@@ -1914,10 +1922,11 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1914
1922
  }
1915
1923
  }
1916
1924
  if (ambient) ambient.hidden = preferences.texture === 'off' && preferences.conversationStyle !== 'deck-chat'
1917
- scheduleFrame()
1925
+ updateFrameGate()
1918
1926
  },
1919
1927
  inspect() {
1920
1928
  return {
1929
+ enabled: fieldEnabled(),
1921
1930
  mounted: Boolean(ambient?.isConnected && canvas?.isConnected),
1922
1931
  hero: heroActive,
1923
1932
  side,
@@ -38,6 +38,8 @@ export const DEFAULT_PREFERENCES = Object.freeze({
38
38
  sessionFlowColorFront: SESSION_FLOW_PALETTES.triad.front,
39
39
  sessionFlowSpeed: SESSION_FLOW_SPEED_DEFAULT,
40
40
  conversationStyle: 'native',
41
+ bakerGroupAvatars: true,
42
+ conversationEmblem: true,
41
43
  })
42
44
 
43
45
  export const VISUAL_PRESETS = Object.freeze({
@@ -79,7 +81,7 @@ export const PREFERENCE_GROUPS = Object.freeze({
79
81
  material: Object.freeze(['glass']),
80
82
  accessibility: Object.freeze(['motion', 'bootAnimation', 'cursor']),
81
83
  navigation: Object.freeze(['railDefaultHidden']),
82
- conversation: Object.freeze(['sessionFlow', 'sessionGlow', 'sessionFlowPalette', 'sessionFlowColorBack', 'sessionFlowColorCore', 'sessionFlowColorFront', 'sessionFlowSpeed', 'conversationStyle']),
84
+ conversation: Object.freeze(['sessionFlow', 'sessionGlow', 'sessionFlowPalette', 'sessionFlowColorBack', 'sessionFlowColorCore', 'sessionFlowColorFront', 'sessionFlowSpeed', 'conversationStyle', 'bakerGroupAvatars', 'conversationEmblem']),
83
85
  })
84
86
 
85
87
  const TEXTURES = new Set(['off', 'restrained', 'full'])
@@ -195,6 +197,8 @@ export function normalizePreferences(value) {
195
197
  sessionFlowColorCore: sessionFlowColors.core,
196
198
  sessionFlowColorFront: sessionFlowColors.front,
197
199
  sessionFlowSpeed: sessionFlowSpeedOr(input.sessionFlowSpeed, DEFAULT_PREFERENCES.sessionFlowSpeed),
200
+ conversationEmblem: booleanOr(input.conversationEmblem, DEFAULT_PREFERENCES.conversationEmblem),
201
+ bakerGroupAvatars: booleanOr(input.bakerGroupAvatars, DEFAULT_PREFERENCES.bakerGroupAvatars),
198
202
  conversationStyle: enumOr(input.conversationStyle, CONVERSATION_STYLES, DEFAULT_PREFERENCES.conversationStyle),
199
203
  }
200
204
  normalized.preset = matchedVisualPreset(normalized)
@@ -51,12 +51,16 @@ function settingButtons(key, label, options) {
51
51
  ? '<small data-prts-setting-note>立即切换会话背景、头像与消息气泡</small>'
52
52
  : key === 'sessionFlow'
53
53
  ? '<small data-prts-setting-note>仅控制当前会话卡片的三色流线</small>'
54
+ : key === 'conversationEmblem'
55
+ ? '<small data-prts-setting-note>控制经典主题正文中的粒子徽记,不影响新会话徽记</small>'
56
+ : key === 'bakerGroupAvatars'
57
+ ? '<small data-prts-setting-note>过程正文仅首条显示头像,最终回答独立保留;关闭后每条回复均显示</small>'
54
58
  : key === 'sessionGlow'
55
59
  ? '<small data-prts-setting-note>为流线添加柔和的同色辉光</small>'
56
60
  : key === 'sessionFlowPalette'
57
61
  ? '<small data-prts-setting-note>同时适配日间与夜间模式</small>'
58
62
  : ''
59
- if (['cursor', 'railDefaultHidden', 'sessionFlow', 'sessionGlow'].includes(key)) return `<div data-prts-setting-row="${key}">
63
+ if (['cursor', 'railDefaultHidden', 'sessionFlow', 'sessionGlow', 'bakerGroupAvatars', 'conversationEmblem'].includes(key)) return `<div data-prts-setting-row="${key}">
60
64
  <span><strong>${label}</strong>${note}</span>
61
65
  <button type="button" role="switch" aria-label="${label}" aria-checked="false" data-prts-setting-switch="${key}" ${key === 'cursor' ? 'data-prts-cursor-switch' : ''}><span aria-hidden="true"></span></button>
62
66
  </div>`
@@ -165,7 +169,9 @@ export function themeSettingsMarkup() {
165
169
  <button type="button" data-prts-setting-key="conversationStyle" data-prts-setting-value="native" aria-pressed="false"><span data-prts-conversation-sample="classic" aria-hidden="true"><i></i><i></i></span><strong>经典</strong><small>简洁正文,保留熟悉的阅读布局</small></button>
166
170
  <button type="button" data-prts-setting-key="conversationStyle" data-prts-setting-value="deck-chat" aria-pressed="false"><span data-prts-conversation-sample="baker" aria-hidden="true"><i></i><i></i></span><strong>Baker</strong><small>终末地通讯风格,圆形头像与消息气泡</small></button>
167
171
  </div>
172
+ <div data-prts-classic-options>${settingButtons('conversationEmblem', '正文徽记', [])}</div>
168
173
  ${avatarMarkup()}
174
+ <div data-prts-baker-options>${settingButtons('bakerGroupAvatars', '合并过程回复头像', [])}</div>
169
175
  </section>
170
176
  </section>
171
177
  <section data-prts-settings-common data-prts-settings-section="interaction" role="tabpanel" id="prts-page-interaction" aria-labelledby="prts-tab-interaction" tabindex="0" hidden>
@@ -688,6 +694,10 @@ export function createThemeSettingsOverlay({
688
694
 
689
695
  return {
690
696
  update(preferences, status, persistenceState = currentPersistence) {
697
+ const emblemNote = panel.querySelector('[data-prts-emblem-baker-note]')
698
+ if (emblemNote) emblemNote.hidden = preferences?.conversationStyle !== 'deck-chat'
699
+ panel.querySelector('[data-prts-classic-options]').hidden = preferences?.conversationStyle === 'deck-chat'
700
+ panel.querySelector('[data-prts-baker-options]').hidden = preferences?.conversationStyle !== 'deck-chat'
691
701
  panel.querySelector('[data-prts-avatars]').hidden = preferences?.conversationStyle !== 'deck-chat'
692
702
  currentPreferences = preferences
693
703
  currentStatus = status
@@ -1346,11 +1346,31 @@ html[data-dsh-prts][data-prts-conversation-style="deck-chat"] :is(
1346
1346
  color: var(--prts-ink) !important;
1347
1347
  }
1348
1348
 
1349
+ /* Bordered Baker tables need inset cells and a scrollport inside the bubble. */
1349
1350
  html[data-dsh-prts][data-prts-conversation-style="deck-chat"] :is(
1350
- [data-prts-ai-surface],
1351
- [data-message-role="assistant"]
1352
- ) blockquote {
1353
- border-inline-start: 3px solid var(--prts-cyan);
1351
+ [data-prts-ai-surface], [data-message-role="assistant"], [data-message-role="user"],
1352
+ :is([data-chat-flow-kind="user"], [data-submission-echo]:not([data-pending-steering])) [class$="_bubble"]
1353
+ ) {
1354
+ & :is(th,td){padding-inline:14px!important}
1355
+ & :is(.md-table-wide,[class*="_tableScroll_"]){
1356
+ box-sizing:border-box;width:100%;max-width:100%;min-width:0;
1357
+ margin-inline:0!important;padding:0!important;overflow:auto;
1358
+ border:1px solid color-mix(in srgb,var(--prts-cyan) 24%,var(--prts-line));
1359
+ border-radius:10px;corner-shape:round;
1360
+ background:color-mix(in srgb,var(--prts-canvas) 88%,transparent);
1361
+ & > table{border:0;background:transparent;margin:0;width:100%}
1362
+ }
1363
+ }
1364
+
1365
+ /* Group process avatars without changing reply alignment or final-answer avatars. */
1366
+ html[data-dsh-prts][data-prts-conversation-style="deck-chat"] [data-prts-avatar-hidden]::before{content:none!important}
1367
+
1368
+ /* Baker quote surfaces; classic prose retains the host styling. */
1369
+ html[data-dsh-prts][data-prts-conversation-style="deck-chat"] :is([data-chat-flow-kind],[data-message-role],[data-prts-ai-surface],[data-submission-echo]) blockquote {
1370
+ border-radius:10px;corner-shape:round;padding:8px 14px;
1371
+ border-inline-start:3px solid var(--prts-cyan)!important;
1372
+ & > :first-child{margin-top:0}
1373
+ & > :last-child{margin-bottom:0}
1354
1374
  }
1355
1375
 
1356
1376
  html[data-dsh-prts] [aria-invalid="true"],
@@ -196,6 +196,8 @@ html[data-dsh-prts] [data-prts-composer-fallback]::after {
196
196
  position: absolute;
197
197
  z-index: 1;
198
198
  inset: 1px 1px 3px;
199
+ border: 0;
200
+ border-radius: 0;
199
201
  background: var(--prts-composer-surface, var(--prts-active-glass));
200
202
  clip-path: polygon(
201
203
  2px 0,
@@ -441,7 +443,7 @@ html[data-dsh-prts] [data-prts-hero-active] [data-composer-seat] {
441
443
  }
442
444
 
443
445
  /* File presentations share one rounded material; preview hit targets stay native. */
444
- html[data-dsh-prts] :is([data-presented-file],[data-prts-conversation-control="file"],[data-produced-files-row] > button[type="button"]){
446
+ html[data-dsh-prts] :is([data-presented-file],[data-changed-files],[data-prts-conversation-control="file"],[data-produced-files-row] > button[type="button"]){
445
447
  border:1px solid var(--prts-glass-border)!important;
446
448
  border-radius:10px!important;corner-shape:round!important;clip-path:none!important;
447
449
  background:var(--prts-active-glass)!important;color:var(--prts-ink);
@@ -449,8 +451,25 @@ html[data-dsh-prts] :is([data-presented-file],[data-prts-conversation-control="f
449
451
  &::before{content:none}
450
452
  &:is(:hover,:focus-within){border-color:var(--prts-yellow)!important}
451
453
  }
452
- /* The host change-card wrapper must not clip a smaller inner outline. */
453
- html[data-dsh-prts] [class$="_card"]:has(> span > [data-prts-conversation-control="file"]){
454
- border:0!important;border-radius:10px!important;corner-shape:round!important;
455
- background:transparent!important;box-shadow:none!important;
454
+ /* One surface for both single-file summaries and grouped changes. */
455
+ html[data-dsh-prts] [data-changed-files]{
456
+ overflow:hidden;
457
+ & button{background:transparent;backdrop-filter:none}
458
+ & button:is(:hover,:focus-visible){background:color-mix(in srgb,var(--prts-yellow) 8%,transparent)}
459
+ & button:focus-visible{box-shadow:inset 0 0 0 2px var(--prts-yellow)}
460
+ }
461
+
462
+ /* Inline file references need their own inset, unlike full delivery cards. */
463
+ html[data-dsh-prts] [data-prts-conversation-control="file"]:is([class*="_fileLink"],[class*="_fileMention"]){
464
+ padding:4px 10px!important;max-width:100%;box-sizing:border-box;
465
+ white-space:normal;overflow-wrap:anywhere;
466
+ }
467
+
468
+ /* Baker user bubbles are light even when the surrounding shell is dark. */
469
+ html[data-dsh-prts][data-prts-conversation-style="deck-chat"] :is([class$="_bubble"],[data-message-role="user"]) [data-ref-chip="file"]{
470
+ background:color-mix(in srgb,var(--prts-cyan) 8%,var(--prts-canvas))!important;
471
+ border-color:color-mix(in srgb,var(--prts-cyan) 28%,transparent)!important;
472
+ color:var(--prts-cyan)!important;backdrop-filter:none!important;
473
+ padding:2px 7px!important;border-radius:6px!important;
474
+ &:is(:hover,:focus-visible){border-color:var(--prts-cyan)!important}
456
475
  }