@ash-qw/dsh-theme-prts 0.1.95 → 0.1.97

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": "0.1.95",
3
+ "version": "0.1.97",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Unofficial Arknights P.R.T.S.-inspired UI theme for DeepSeek Harness Web",
@@ -67,7 +67,8 @@
67
67
  "prepublishOnly": "npm run check"
68
68
  },
69
69
  "engines": {
70
- "node": ">=18"
70
+ "node": ">=18",
71
+ "dsh": ">=0.1.2-rc.1 <0.1.3-0 || >=0.1.5-rc.1 <0.1.6-0"
71
72
  },
72
73
  "devDependencies": {
73
74
  "@playwright/test": "1.55.1",
@@ -1,5 +1,6 @@
1
1
  export const ASSISTANT_SURFACE_ATTRIBUTE = 'data-prts-ai-surface'
2
2
  export const ASSISTANT_STATE_ATTRIBUTE = 'data-prts-ai-surface-state'
3
+ export const ASSISTANT_AVATAR_PROPERTY = '--prts-assistant-avatar-image'
3
4
 
4
5
  const STEP_SELECTOR = '[data-chat-flow-kind="assistant-step"]'
5
6
  const CONTENT_HINT_SELECTOR = [
@@ -16,6 +17,7 @@ const EXCLUDED_SELECTOR = [
16
17
  ].join(', ')
17
18
  const SCAN_CHUNK_LIMIT = 64
18
19
  const SCAN_BUDGET_MS = 4
20
+ const INITIAL_LATEST_LIMIT = 12
19
21
 
20
22
  function isElement(node) {
21
23
  return node?.nodeType === 1
@@ -90,13 +92,42 @@ export function resolveAssistantSurface(step) {
90
92
  return resolution(step).surface
91
93
  }
92
94
 
93
- export function createAssistantGlassAdapter({ document, window }) {
95
+ function cssUrl(value) {
96
+ if (!value) return ''
97
+ const escaped = String(value)
98
+ .replaceAll('\\', '\\\\')
99
+ .replaceAll('"', '\\"')
100
+ .replaceAll('\n', '')
101
+ return `url("${escaped}")`
102
+ }
103
+
104
+ export function createAssistantGlassAdapter({ document, window, avatarImage = '' }) {
94
105
  let observer
95
106
  let scanFrame
96
107
  const ownedSurfaces = new Set()
97
108
  const ownedSteps = new Set()
98
109
  const stepSurfaces = new Map()
99
110
  const pendingSteps = new Set()
111
+ let previousAvatarImage
112
+ let ownsAvatarImage = false
113
+
114
+ function mountAvatarImage() {
115
+ const root = document?.documentElement
116
+ const value = cssUrl(avatarImage)
117
+ if (!root?.style || !value || ownsAvatarImage) return
118
+ previousAvatarImage = root.style.getPropertyValue(ASSISTANT_AVATAR_PROPERTY)
119
+ root.style.setProperty(ASSISTANT_AVATAR_PROPERTY, value)
120
+ ownsAvatarImage = true
121
+ }
122
+
123
+ function releaseAvatarImage() {
124
+ if (!ownsAvatarImage) return
125
+ const root = document?.documentElement
126
+ if (previousAvatarImage) root?.style?.setProperty(ASSISTANT_AVATAR_PROPERTY, previousAvatarImage)
127
+ else root?.style?.removeProperty(ASSISTANT_AVATAR_PROPERTY)
128
+ previousAvatarImage = undefined
129
+ ownsAvatarImage = false
130
+ }
100
131
 
101
132
  function collectSteps(node, includeParent = true) {
102
133
  if (!isElement(node)) return
@@ -108,6 +139,23 @@ export function createAssistantGlassAdapter({ document, window }) {
108
139
  }
109
140
  }
110
141
 
142
+ function collectInitialSteps(node) {
143
+ if (!isElement(node)) return
144
+ const steps = [...(node.querySelectorAll?.(STEP_SELECTOR) ?? [])]
145
+ if (node.matches?.(STEP_SELECTOR)) steps.unshift(node)
146
+ const priorityStart = Math.max(0, steps.length - INITIAL_LATEST_LIMIT)
147
+
148
+ // The host restores conversations in chronological DOM order. Resolve the
149
+ // newest screenful synchronously so the startup cover never reveals plain
150
+ // assistant content while an older history backlog is still being scanned.
151
+ for (let index = steps.length - 1; index >= priorityStart; index -= 1) {
152
+ processStep(steps[index])
153
+ }
154
+ for (let index = priorityStart - 1; index >= 0; index -= 1) {
155
+ pendingSteps.add(steps[index])
156
+ }
157
+ }
158
+
111
159
  function stableBodyContains(step, node) {
112
160
  const surface = stepSurfaces.get(step)
113
161
  return step?.getAttribute?.(ASSISTANT_STATE_ATTRIBUTE) === 'body'
@@ -201,12 +249,13 @@ export function createAssistantGlassAdapter({ document, window }) {
201
249
  return {
202
250
  start() {
203
251
  if (!document) return
252
+ mountAvatarImage()
204
253
  const observationRoot = document.querySelector('[data-prts-region="operation"]')
205
- ?? document.querySelector('[data-slot="conversation"]')
254
+ ?? document.querySelector('[data-slot="main.conversation"], [data-slot="conversation"]')
206
255
  if (!observationRoot) return
207
- collectSteps(observationRoot)
208
- scan()
209
256
  if (observer) return
257
+ collectInitialSteps(observationRoot)
258
+ scan()
210
259
  const Observer = window?.MutationObserver
211
260
  if (!Observer) return
212
261
  observer = new Observer(onMutations)
@@ -226,6 +275,7 @@ export function createAssistantGlassAdapter({ document, window }) {
226
275
  }
227
276
  scanFrame = undefined
228
277
  removeMarkers()
278
+ releaseAvatarImage()
229
279
  },
230
280
  }
231
281
  }
@@ -187,7 +187,7 @@ export function createComposerGlassAdapter({ document, window }) {
187
187
  const Observer = window?.MutationObserver
188
188
  if (!Observer) return
189
189
  const observationRoot = document.querySelector('[data-prts-region="operation"]')
190
- ?? document.querySelector('[data-slot="conversation"]')
190
+ ?? document.querySelector('[data-slot="main.conversation"], [data-slot="conversation"]')
191
191
  if (!observationRoot) return
192
192
  observer = new Observer(onMutations)
193
193
  observer.observe(observationRoot, {
@@ -46,7 +46,11 @@ export function applyPrtsPlugin(ctx, environment) {
46
46
  const themeService = contextService(ctx, 'theme')
47
47
  const startup = createPrtsStartupSequence({ document, window, prtsEmblem: assets.prtsEmblem, rhodesEmblem: assets.emblem, timings: startupTimings })
48
48
  const composerGlass = createComposerGlassAdapter({ document, window })
49
- const assistantGlass = createAssistantGlassAdapter({ document, window })
49
+ const assistantGlass = createAssistantGlassAdapter({
50
+ document,
51
+ window,
52
+ avatarImage: assets.prtsEmblem,
53
+ })
50
54
  const particleField = createParticleFieldAdapter({
51
55
  document,
52
56
  window,
@@ -206,7 +210,8 @@ export function applyPrtsPlugin(ctx, environment) {
206
210
  }
207
211
  particleField.update(preferences)
208
212
  composerGlass.start()
209
- assistantGlass.start()
213
+ if (preferences.conversationStyle === 'deck-chat') assistantGlass.start()
214
+ else assistantGlass.dispose()
210
215
  toBottom.start()
211
216
  conversationControls.start()
212
217
  layoutDiagnostics.start()
@@ -1877,7 +1877,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1877
1877
  mutationObserver.observe(observationRoot, { childList: true, subtree: true })
1878
1878
  }
1879
1879
  }
1880
- if (ambient) ambient.hidden = preferences.texture === 'off'
1880
+ if (ambient) ambient.hidden = preferences.texture === 'off' && preferences.conversationStyle !== 'deck-chat'
1881
1881
  scheduleFrame()
1882
1882
  },
1883
1883
  inspect() {
@@ -6,7 +6,7 @@ export const PARTICLE_TRAVERSAL_SPEED_STEP = 0.25
6
6
  export const PARTICLE_TRAVERSAL_SPEED_DEFAULT = 1
7
7
 
8
8
  export const DEFAULT_PREFERENCES = Object.freeze({
9
- version: 8,
9
+ version: 9,
10
10
  enabled: false,
11
11
  preset: 'standard-tactical',
12
12
  texture: 'full',
@@ -18,6 +18,7 @@ export const DEFAULT_PREFERENCES = Object.freeze({
18
18
  particleTraversalSpeed: PARTICLE_TRAVERSAL_SPEED_DEFAULT,
19
19
  particlePattern: 'orthogonal',
20
20
  railDefaultHidden: false,
21
+ conversationStyle: 'deck-chat',
21
22
  })
22
23
 
23
24
  export const VISUAL_PRESETS = Object.freeze({
@@ -59,6 +60,7 @@ export const PREFERENCE_GROUPS = Object.freeze({
59
60
  material: Object.freeze(['glass']),
60
61
  accessibility: Object.freeze(['motion', 'bootAnimation']),
61
62
  navigation: Object.freeze(['railDefaultHidden']),
63
+ conversation: Object.freeze(['conversationStyle']),
62
64
  })
63
65
 
64
66
  const TEXTURES = new Set(['off', 'restrained', 'full'])
@@ -66,6 +68,7 @@ const GLASS_STRENGTHS = new Set(['off', 'soft', 'standard', 'clear'])
66
68
  const MOTIONS = new Set(['system', 'reduced'])
67
69
  const PARTICLE_DENSITIES = new Set(['sparse', 'light', 'standard', 'dense', 'ultra'])
68
70
  const PRESETS = new Set([...Object.keys(VISUAL_PRESETS), 'custom'])
71
+ const CONVERSATION_STYLES = new Set(['native', 'deck-chat'])
69
72
  const PRESET_LINKED_KEYS = new Set(Object.keys(VISUAL_PRESETS['standard-tactical']))
70
73
 
71
74
 
@@ -129,7 +132,7 @@ export function normalizePreferences(value) {
129
132
  const particlePreset = VISUAL_PRESETS[input.preset] ?? DEFAULT_PREFERENCES
130
133
  const legacyDensities = legacyParticleDensities(input)
131
134
  const normalized = {
132
- version: 8,
135
+ version: 9,
133
136
  enabled: booleanOr(input.enabled, DEFAULT_PREFERENCES.enabled),
134
137
  preset: enumOr(migratedPreset, PRESETS, DEFAULT_PREFERENCES.preset),
135
138
  texture: enumOr(input.texture, TEXTURES, DEFAULT_PREFERENCES.texture),
@@ -144,6 +147,7 @@ export function normalizePreferences(value) {
144
147
  ),
145
148
  particlePattern: 'orthogonal',
146
149
  railDefaultHidden: booleanOr(input.railDefaultHidden, DEFAULT_PREFERENCES.railDefaultHidden),
150
+ conversationStyle: enumOr(input.conversationStyle, CONVERSATION_STYLES, DEFAULT_PREFERENCES.conversationStyle),
147
151
  }
148
152
  normalized.preset = matchedVisualPreset(normalized)
149
153
  return normalized
@@ -190,7 +194,7 @@ export function loadPreferences(storage) {
190
194
  const raw = storage?.getItem(PRTS_STORAGE_KEY)
191
195
  if (raw === null || raw === undefined) return { ...DEFAULT_PREFERENCES }
192
196
  const parsed = JSON.parse(raw)
193
- if (![1, 2, 3, 4, 5, 6, 7, 8].includes(parsed?.version)) return { ...DEFAULT_PREFERENCES }
197
+ if (![1, 2, 3, 4, 5, 6, 7, 8, 9].includes(parsed?.version)) return { ...DEFAULT_PREFERENCES }
194
198
  return normalizePreferences(parsed)
195
199
  } catch {
196
200
  return { ...DEFAULT_PREFERENCES }
@@ -7,36 +7,73 @@ function closestCommonContainer(root, nodes) {
7
7
  return root && nodes.every(node => root.contains(node)) ? root : null
8
8
  }
9
9
 
10
+ const LAYOUT_CONTRACTS = [
11
+ {
12
+ version: '0.1.5',
13
+ center: '[data-slot="main"]',
14
+ auxiliary: '[data-slot="rightbar"]',
15
+ },
16
+ {
17
+ version: '0.1.2',
18
+ center: '[data-slot="conversation"]',
19
+ auxiliary: '[data-slot="details"]',
20
+ },
21
+ ]
22
+
10
23
  export function resolveRc7Regions(document, report = () => {}) {
11
24
  const root = document?.querySelector('[data-slot="root"]')
12
25
  const sidebarSlot = root?.querySelector('[data-slot="sidebar"]')
13
- const conversationSlot = root?.querySelector('[data-slot="conversation"]')
14
- const detailsSlot = root?.querySelector('[data-slot="details"]')
15
- if (!root || !sidebarSlot || !conversationSlot || !detailsSlot) {
26
+ const contract = LAYOUT_CONTRACTS
27
+ .map(candidate => ({
28
+ ...candidate,
29
+ centerSlot: root?.querySelector(candidate.center),
30
+ auxiliarySlot: root?.querySelector(candidate.auxiliary),
31
+ }))
32
+ .find(candidate => candidate.centerSlot && candidate.auxiliarySlot)
33
+ if (!root || !sidebarSlot || !contract) {
16
34
  report('required layout slots are unavailable', {
17
35
  root: Boolean(root), sidebar: Boolean(sidebarSlot),
18
- conversation: Boolean(conversationSlot), details: Boolean(detailsSlot),
36
+ main: Boolean(root?.querySelector('[data-slot="main"]')),
37
+ rightbar: Boolean(root?.querySelector('[data-slot="rightbar"]')),
38
+ conversation: Boolean(root?.querySelector('[data-slot="conversation"]')),
39
+ details: Boolean(root?.querySelector('[data-slot="details"]')),
19
40
  })
20
41
  return null
21
42
  }
43
+ const { centerSlot, auxiliarySlot } = contract
22
44
  const sidebar = sidebarSlot?.parentElement
23
- const center = conversationSlot?.parentElement
24
- const details = detailsSlot?.parentElement
45
+ const center = centerSlot?.parentElement
46
+ const auxiliary = auxiliarySlot?.parentElement
25
47
 
26
- if (!sidebar || !center || !details) {
48
+ if (!sidebar || !center || !auxiliary) {
27
49
  report('required layout region containers are unavailable', {
28
- sidebar: Boolean(sidebar), conversation: Boolean(center), details: Boolean(details),
50
+ sidebar: Boolean(sidebar), center: Boolean(center), auxiliary: Boolean(auxiliary),
29
51
  })
30
52
  return null
31
53
  }
32
54
 
33
- const frame = closestCommonContainer(root, [sidebar, center, details])
55
+ const frame = closestCommonContainer(root, [sidebar, center, auxiliary])
34
56
  if (!frame) {
35
57
  report('common layout container is unavailable')
36
58
  return null
37
59
  }
38
60
 
39
- return { root, frame, sidebar, center, details, sidebarSlot, conversationSlot, detailsSlot }
61
+ return {
62
+ root,
63
+ frame,
64
+ sidebar,
65
+ center,
66
+ auxiliary,
67
+ sidebarSlot,
68
+ centerSlot,
69
+ auxiliarySlot,
70
+ layoutVersion: contract.version,
71
+ // Legacy aliases keep the internal adapter face compatible with callers
72
+ // built against the 0.1.2 layout vocabulary.
73
+ conversationSlot: centerSlot,
74
+ details: auxiliary,
75
+ detailsSlot: auxiliarySlot,
76
+ }
40
77
  }
41
78
 
42
79
  export function createRc7Adapter({ document, warn } = {}) {
@@ -64,7 +101,7 @@ export function createRc7Adapter({ document, warn } = {}) {
64
101
  [regions.frame, 'frame'],
65
102
  [regions.sidebar, 'sessions'],
66
103
  [regions.center, 'operation'],
67
- [regions.details, 'details'],
104
+ [regions.auxiliary, 'auxiliary'],
68
105
  ]) {
69
106
  owned.push([node, node.getAttribute('data-prts-region')])
70
107
  node.setAttribute('data-prts-region', value)
@@ -7,6 +7,7 @@ const ROOT_ATTRIBUTES = [
7
7
  'data-prts-glass',
8
8
  'data-prts-motion',
9
9
  'data-prts-particle-pattern',
10
+ 'data-prts-conversation-style',
10
11
  ]
11
12
 
12
13
  const TRANSITION_ATTRIBUTE = 'data-prts-scheme-transition'
@@ -715,6 +716,7 @@ export function createThemeController({ document, window, cssText, service, onTr
715
716
  root.dataset.prtsGlass = preferences.glass
716
717
  root.dataset.prtsMotion = preferences.motion
717
718
  root.dataset.prtsParticlePattern = 'orthogonal'
719
+ root.dataset.prtsConversationStyle = preferences.conversationStyle
718
720
  refresh()
719
721
  },
720
722
  sync,
@@ -7,6 +7,7 @@ const ROOT_ATTRIBUTES = [
7
7
  'data-prts-glass',
8
8
  'data-prts-motion',
9
9
  'data-prts-particle-pattern',
10
+ 'data-prts-conversation-style',
10
11
  ]
11
12
 
12
13
  function themeIdentity(value) {
@@ -108,6 +109,7 @@ export function createThemeController({ document, window, cssText, service }) {
108
109
  root.dataset.prtsGlass = preferences.glass
109
110
  root.dataset.prtsMotion = preferences.motion
110
111
  root.dataset.prtsParticlePattern = 'orthogonal'
112
+ root.dataset.prtsConversationStyle = preferences.conversationStyle
111
113
  refresh()
112
114
  },
113
115
  sync,
@@ -19,6 +19,7 @@ const COMMON_CONTROLS = [
19
19
  ['texture', '环境底纹', [['off', '关闭'], ['restrained', '克制'], ['full', '完整']]],
20
20
  ['glass', '玻璃材质', [['off', '关闭'], ['soft', '柔和'], ['standard', '标准'], ['clear', '清晰']]],
21
21
  ['motion', '动态效果', [['system', '跟随系统'], ['reduced', '减少']]],
22
+ ['conversationStyle', '会话主题', [['native', '原有'], ['deck-chat', '通讯链路']]],
22
23
  ]
23
24
 
24
25
  const PARTICLE_OPTIONS = [
@@ -34,6 +35,8 @@ function settingButtons(key, label, options) {
34
35
  ? '<small data-prts-setting-note data-prts-effective-motion hidden>系统当前要求减少动态</small>'
35
36
  : key === 'railDefaultHidden'
36
37
  ? '<small data-prts-setting-note>在任意窗口宽度下默认收起 P.R.T.S. 导航,可从左侧按钮临时展开</small>'
38
+ : key === 'conversationStyle'
39
+ ? '<small data-prts-setting-note>立即切换会话背景、头像与消息气泡</small>'
37
40
  : ''
38
41
  const sample = value => key === 'texture' || key === 'glass'
39
42
  ? `<i data-prts-option-sample="${key}" data-prts-option-sample-value="${value}" aria-hidden="true"></i>`