@ash-qw/dsh-theme-prts 2.5.2 → 2.5.4

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.5.2",
3
+ "version": "2.5.4",
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",
@@ -5,7 +5,16 @@ export function createBackgroundLayout({window,document}) {
5
5
  const raf=window.requestAnimationFrame?.bind(window) ?? (fn=>window.setTimeout(fn,16))
6
6
  const caf=window.cancelAnimationFrame?.bind(window) ?? window.clearTimeout.bind(window)
7
7
  let state={}, frame, disposed=false, enabled=false, observing=false, hostFrame, operation, sessions
8
- const set=(name,value)=>{if(root.style.getPropertyValue(name)!==value) root.style.setProperty(name,value)}
8
+ const sidebarTargets = new Set()
9
+ const set=(name,value)=>{
10
+ const targets=name.startsWith('--prts-protected-sidebar-')?[sessions,document.querySelector('[data-prts-nav-rail]')]:[root]
11
+ for(const node of targets){
12
+ if(!node)continue
13
+ if(node!==root)sidebarTargets.add(node)
14
+ if(value===null)node.style.removeProperty(name)
15
+ else if(node.style.getPropertyValue(name)!==value)node.style.setProperty(name,value)
16
+ }
17
+ }
9
18
  const update=()=>{
10
19
  frame=undefined
11
20
  if(disposed||!observing)return
@@ -17,6 +26,8 @@ export function createBackgroundLayout({window,document}) {
17
26
  for(const node of [hostFrame||operation,sessions])if(node)resize?.observe(node)
18
27
  }
19
28
  const railWidth=parseFloat(window.getComputedStyle(root).getPropertyValue('--prts-nav-width'))||0
29
+ const writes=[]
30
+ const write=(...args)=>writes.push(args)
20
31
  for(const slot of ['full','conversation','sidebar']){
21
32
  const entry=state[slot]
22
33
  const node=slot==='sidebar'?sessions:slot==='conversation'?(hostFrame||operation):operation
@@ -25,15 +36,16 @@ export function createBackgroundLayout({window,document}) {
25
36
  const active=entry?.region && width>0 && height>0
26
37
  if(active){
27
38
  const p=backgroundPlacement(entry.width,entry.height,width,height,entry.region)
28
- set(`--prts-protected-${slot}-x`,`${p.x}px`)
29
- set(`--prts-protected-${slot}-y`,`${p.y}px`)
30
- if(slot==='sidebar')set('--prts-protected-sidebar-width',`${p.width}px`)
39
+ write(`--prts-protected-${slot}-x`,`${p.x}px`)
40
+ write(`--prts-protected-${slot}-y`,`${p.y}px`)
41
+ if(slot==='sidebar')write('--prts-protected-sidebar-width',`${p.width}px`)
31
42
  }else{
32
- root.style.removeProperty(`--prts-protected-${slot}-x`)
33
- root.style.removeProperty(`--prts-protected-${slot}-y`)
34
- if(slot==='sidebar')root.style.removeProperty('--prts-protected-sidebar-width')
43
+ write(`--prts-protected-${slot}-x`,null)
44
+ write(`--prts-protected-${slot}-y`,null)
45
+ if(slot==='sidebar')write('--prts-protected-sidebar-width',null)
35
46
  }
36
47
  }
48
+ for(const args of writes)set(...args)
37
49
  }
38
50
  function schedule(){if(!disposed && observing && frame===undefined)frame=raf(update)}
39
51
  // ResizeObserver runs after layout, before paint. Deferring again to rAF
@@ -48,6 +60,8 @@ export function createBackgroundLayout({window,document}) {
48
60
  const attributes=new window.MutationObserver(schedule)
49
61
  function clear(){
50
62
  caf(frame);frame=undefined;resize?.disconnect();mutations.disconnect();attributes.disconnect();window.removeEventListener('resize',schedule)
63
+ for(const node of sidebarTargets)for(const axis of ['x','y','width'])node.style.removeProperty(`--prts-protected-sidebar-${axis}`)
64
+ sidebarTargets.clear()
51
65
  hostFrame=undefined;operation=undefined;sessions=undefined;observing=false
52
66
  for(const slot of ['full','conversation','sidebar'])for(const axis of ['x','y','width'])root.style.removeProperty(`--prts-protected-${slot}-${axis}`)
53
67
  }
@@ -11,6 +11,15 @@ function closestWithClassSuffix(node, suffix) {
11
11
  return null
12
12
  }
13
13
 
14
+ function isPendingInteractionAction(button) {
15
+ if (!button || button.tagName !== 'BUTTON') return false
16
+ if (button.closest('[data-approval-key]')) return true
17
+ if (button.closest('[data-question-key]')) {
18
+ return hasClassSuffix(button, '_waitButton') || Boolean(closestWithClassSuffix(button, '_footerActions'))
19
+ }
20
+ return Boolean(button.closest('[data-plan-review-key]') && closestWithClassSuffix(button, '_actions'))
21
+ }
22
+
14
23
  function classify(button, operation) {
15
24
  // The host's process disclosure owns its transparent, full-width hit area.
16
25
  if (button.hasAttribute('data-turn-process')) return null
@@ -40,6 +49,8 @@ function classify(button, operation) {
40
49
  if (approval) return hasClassSuffix(button, '_reject') ? 'danger' : 'cta'
41
50
  if (hasClassSuffix(button, '_modalAction') || button.closest('[role="dialog"]')) return 'cta'
42
51
 
52
+ const userRow = closestWithClassSuffix(button, '_userRow')
53
+ if (userRow?.matches('[data-submission-echo], [data-pending-steering]')) return 'micro'
43
54
  if (button.closest('[data-chat-flow-kind], [data-message-role]')) return 'micro'
44
55
  return null
45
56
  }
@@ -120,7 +131,7 @@ export function createConversationControlAdapter({ document, window }) {
120
131
  subtree: true,
121
132
  childList: true,
122
133
  attributes: true,
123
- attributeFilter: ['class', 'data-approval-key', 'data-turn-process'],
134
+ attributeFilter: ['class', 'data-approval-key', 'data-turn-process', 'data-submission-echo', 'data-pending-steering', 'data-chat-flow-kind', 'data-message-role'],
124
135
  })
125
136
  },
126
137
  dispose() {
@@ -138,14 +149,16 @@ function mountBodyTargetFrame(document, window, operation) {
138
149
  // Nonvisual hosts can still mount the theme without an animation clock.
139
150
  if (!window?.requestAnimationFrame || !window?.cancelAnimationFrame) return () => {}
140
151
  const composerButton=':is([data-composer-card],[data-prts-composer-fallback]) button:not([data-slot="conversation.input.attachments"] *,[data-prts-glass-menu] *,[data-prts-floating-glass] *,[role="menu"] *,[role="listbox"] *)'
141
- const selector='[data-prts-conversation-control],[data-presented-file],[data-changed-files],[data-produced-files-row] > button,[data-prts-glass-control],[data-prts-footer-action],[data-prts-transfer] button,'+composerButton
152
+ const selector='[data-prts-to-bottom],[data-prts-conversation-control],[data-presented-file],[data-changed-files],[data-produced-files-row] > button,[data-prts-glass-control],[data-prts-footer-action],[data-prts-transfer] button,'+composerButton
142
153
  const frame=document.createElement('div')
143
154
  frame.setAttribute('data-prts-body-target-frame','');frame.setAttribute('aria-hidden','true')
144
155
  const corners=Array.from({length:4},()=>frame.appendChild(document.createElement('i')))
145
156
  document.body.append(frame)
146
157
  if(frame.showPopover)frame.setAttribute('popover','manual')
147
- let layer=null
148
- const hideFrame=()=>{if(frame.hidePopover && frame.matches(':popover-open'))frame.hidePopover()}
158
+ let layer=null, imageRetiring=false
159
+ const hideFrame=()=>{
160
+ if(frame.hidePopover && frame.matches(':popover-open'))frame.hidePopover()
161
+ }
149
162
  const showFrame=()=>{
150
163
  if(!frame.showPopover)return
151
164
  const modal=target?.closest('dialog[open]')
@@ -153,46 +166,66 @@ function mountBodyTargetFrame(document, window, operation) {
153
166
  if(!frame.matches(':popover-open'))frame.showPopover()
154
167
  layer=modal
155
168
  }
156
- let target=null, hovered=null, focused=null, raf=0, pressed=false, disposed=false
157
- let pointer={x:0,y:0}, state=null, settled=false, tween=null, bounds=null, boundsDirty=false, hitDirty=false, exitGoal=null, motionDuration=240, targetArm=7
158
- const resizeObserver=window.ResizeObserver?new window.ResizeObserver(()=>{boundsDirty=true;wake()}):null
169
+ let target=null, hovered=null, focused=null, raf=0, pressed=false, releasePending=false, disposed=false
170
+ let pointer={x:0,y:0}, state=null, settled=false, tween=null, bounds=null, boundsDirty=false, hitDirty=false, exitGoal=null, motionDuration=240, targetArm=7, retiringBounds=null, targetRetires=false
171
+ let moveRaf=0, pendingMove=null
172
+ const resizeObserver=window.ResizeObserver?new window.ResizeObserver(entries=>{
173
+ if(!target)return
174
+ const entry=entries.find(item=>item.target===target),size=entry?.borderBoxSize?.[0]
175
+ if(size && bounds && Math.abs(size.inlineSize-bounds.width)<.5 && Math.abs(size.blockSize-bounds.height)<.5)return
176
+ const box=target.getBoundingClientRect()
177
+ if(bounds && ['left','top','width','height'].every(key=>bounds[key]===box[key]))return
178
+ boundsDirty=true;wake()
179
+ }):null
159
180
  const reduced=window.matchMedia?.('(prefers-reduced-motion: reduce)')
160
181
  const fine=window.matchMedia?.('(hover:hover) and (pointer:fine)')
182
+ let activeModal=null
161
183
  const find=node=>{
184
+ if(activeModal && !activeModal.contains(node))return null
162
185
  if (!node?.closest || node.closest('[data-prts-workspace-actions],[data-prts-session-actions]')) return null
163
186
  const card=node.closest('[data-presented-file],[data-changed-files]')
164
- const candidate=card || node.closest(selector)
187
+ const button=node.closest('button')
188
+ const pendingAction=operation.contains(button) && isPendingInteractionAction(button) ? button : null
189
+ const candidate=card || pendingAction || node.closest(selector)
165
190
  return candidate && !candidate.matches(':disabled,[aria-disabled="true"]') && !candidate.closest('[inert]') ? candidate : null
166
191
  }
167
- const measure=()=>{bounds=target.getBoundingClientRect();targetArm=7+Math.max(0,Math.min(1,(Math.min(bounds.width,bounds.height)-36)/36))*5;boundsDirty=false}
192
+ const measure=(box)=>{bounds=box || target.getBoundingClientRect();targetArm=7+Math.max(0,Math.min(1,(Math.min(bounds.width,bounds.height)-36)/36))*5;boundsDirty=false}
168
193
  const wake=()=>{if(!raf && !disposed)raf=window.requestAnimationFrame(draw)}
169
194
  const choose=()=>{
170
- const next=hovered || focused
195
+ const next=(hovered?.isConnected?hovered:null) || (focused?.isConnected?focused:null)
171
196
  if(next!==target){
197
+ const nextBounds=next?.getBoundingClientRect()
198
+ const retire=!next && target && !target.isConnected && targetRetires && releasePending && bounds && !reduced?.matches
199
+ imageRetiring=false
200
+ retiringBounds=retire?{left:bounds.left,top:bounds.top,width:bounds.width,height:bounds.height}:null
172
201
  target?.removeAttribute('data-prts-corner-target')
173
- pressed=false;settled=false;tween=null;exitGoal=null;motionDuration=240;target=next;boundsDirty=true
202
+ if(retire){hovered=focused=target=null;boundsDirty=false;resizeObserver?.disconnect();wake();return}
203
+ pressed=false;releasePending=false;settled=false;tween=null;exitGoal=null;motionDuration=240;target=next;boundsDirty=true
174
204
  resizeObserver?.disconnect()
175
205
  if(target)resizeObserver?.observe(target)
176
206
  if(target){
207
+ targetRetires=hasClassSuffix(target,'_toBottom') || isPendingInteractionAction(target)
177
208
  target.setAttribute('data-prts-corner-target','');showFrame()
178
- measure();const box=bounds
179
- if(!state || state.alpha<.01)state={x:(hovered?pointer.x:box.left+box.width/2)-10,y:(hovered?pointer.y:box.top+box.height/2)-10,w:20,h:20,alpha:0,arm:7}
180
- const style=window.getComputedStyle(target)
181
- frame.style.color=style.getPropertyValue(target.getAttribute('data-prts-conversation-control')==='danger'?'--prts-red':'--prts-yellow')
209
+ measure(nextBounds);const box=bounds
210
+ state={x:(hovered?pointer.x:box.left+box.width/2)-10,y:(hovered?pointer.y:box.top+box.height/2)-10,w:20,h:20,alpha:0,arm:7}
211
+ frame.style.opacity='0'
212
+ frame.style.color=target.getAttribute('data-prts-conversation-control')==='danger'?window.getComputedStyle(target).getPropertyValue('--prts-red'):'var(--prts-corner-color, var(--prts-yellow))'
182
213
  }
183
214
  }
215
+ if(!target && !state?.alpha)hideFrame()
184
216
  if(!settled || !target && state?.alpha>0)wake()
185
217
  }
186
218
  function draw(time) {
187
219
  raf=0
220
+ if(target && !target.isConnected)choose()
188
221
  if(hitDirty){hitDirty=false;hovered=find(document.elementFromPoint(pointer.x,pointer.y));choose()}
189
- if(target && !target.isConnected){target.removeAttribute('data-prts-corner-target');target=hovered=focused=null;pressed=false;settled=false;exitGoal=null}
222
+ if(target && !target.isConnected){target.removeAttribute('data-prts-corner-target');target=hovered=focused=null;pressed=false;releasePending=false;settled=false;exitGoal=null}
190
223
  if(!state)return
191
224
  if(target && boundsDirty){
192
225
  measure()
193
- if(!bounds.width && !bounds.height){target.removeAttribute('data-prts-corner-target');target=hovered=focused=null;pressed=false;settled=false;exitGoal=null;resizeObserver?.disconnect()}
226
+ if(!bounds.width && !bounds.height){target.removeAttribute('data-prts-corner-target');target=hovered=focused=null;pressed=false;releasePending=false;settled=false;exitGoal=null;resizeObserver?.disconnect()}
194
227
  }
195
- const box=target?bounds:null,gap=pressed?3:5
228
+ const box=target?bounds:retiringBounds,gap=pressed?(imageRetiring?0:3):5
196
229
  // Freeze the exit destination: subsequent pointer motion must not restart the fade.
197
230
  const goal=box?{x:box.left-gap,y:box.top-gap,w:box.width+gap*2,h:box.height+gap*2,alpha:1,arm:targetArm}:
198
231
  (exitGoal ||= {x:pointer.x-10,y:pointer.y-10,w:20,h:20,alpha:0,arm:7})
@@ -206,7 +239,7 @@ function mountBodyTargetFrame(document, window, operation) {
206
239
  tween={from:{...state},to:{...goal},start:time,duration:remaining,pressed}
207
240
  }
208
241
  // All channels share one endpoint; no separate thresholds or late position snap.
209
- const progress=reduced?.matches || settled || !tween.duration?1:Math.min(1,(time-tween.start)/tween.duration)
242
+ const progress=reduced?.matches || settled || !tween.duration || (!state.alpha && !goal.alpha)?1:Math.min(1,(time-tween.start)/tween.duration)
210
243
  const ease=1-(1-progress)**3
211
244
  for(const key of keys)state[key]=tween.from[key]+(goal[key]-tween.from[key])*ease
212
245
  const moving=progress<1
@@ -223,38 +256,82 @@ function mountBodyTargetFrame(document, window, operation) {
223
256
  if(frame.style.opacity!==opacity)frame.style.opacity=opacity
224
257
  settled=!moving
225
258
  frame.toggleAttribute('data-pressed',pressed)
226
- if(moving)wake()
259
+ if(releasePending && pressed && !moving){
260
+ if(retiringBounds){exitGoal={...state,alpha:0};retiringBounds=null}
261
+ releasePending=false;pressed=false;motionDuration=imageRetiring?120:140;settled=false;wake()
262
+ }
263
+ else if(moving)wake()
227
264
  else {tween=null;if(!state.alpha)hideFrame()}
228
265
  }
229
- const move=event=>{
266
+ const applyMove=event=>{
230
267
  if(document.hidden || event.pointerType==='touch' || !fine?.matches)return
231
268
  pointer={x:event.clientX,y:event.clientY}
232
269
  const next=find(event.target)
233
270
  if(next===hovered && (next || !state?.alpha))return
234
271
  hovered=next;choose()
235
272
  }
236
- const leave=()=>{hovered=null;choose()}
273
+ const clearMove=()=>{window.cancelAnimationFrame(moveRaf);moveRaf=0;pendingMove=null}
274
+ const flushMove=()=>{
275
+ const event=pendingMove;clearMove()
276
+ if(event)applyMove(event)
277
+ }
278
+ const move=event=>{
279
+ if(document.hidden || event.pointerType==='touch' || !fine?.matches)return
280
+ pendingMove={target:event.target,clientX:event.clientX,clientY:event.clientY,pointerType:event.pointerType}
281
+ if(!moveRaf)moveRaf=window.requestAnimationFrame(flushMove)
282
+ }
283
+ const leave=()=>{clearMove();hovered=null;choose()}
237
284
  const focus=event=>{focused=event.target.matches?.(':focus-visible')?find(event.target):null;choose()}
238
285
  const blur=()=>{focused=null;choose()}
239
286
  const down=event=>{
287
+ flushMove()
240
288
  pointer={x:event.clientX,y:event.clientY}
241
289
  if(event.pointerType==='touch')hovered=find(event.target)
242
290
  choose()
243
- if(target && find(event.target)===target){pressed=true;motionDuration=100;settled=false;wake()}
291
+ if(target && find(event.target)===target){pressed=true;releasePending=false;motionDuration=100;settled=false;wake()}
292
+ }
293
+ const release=()=>{
294
+ // A tap can release before the first paint. Finish one contraction, then
295
+ // expand; one flag keeps repeated taps interruptible without a timer queue.
296
+ if(pressed && !settled && !reduced?.matches){releasePending=true;settled=false;wake()}
297
+ else {pressed=false;releasePending=false;motionDuration=140;settled=false;wake()}
244
298
  }
245
- const up=event=>{pressed=false;motionDuration=140;settled=false;if(event.pointerType==='touch')hovered=null;choose()}
299
+ const up=event=>{release();if(event.pointerType==='touch')hovered=null;choose()}
246
300
  const reset=()=>{
301
+ clearMove()
247
302
  target?.removeAttribute('data-prts-corner-target');hideFrame()
248
- hovered=focused=target=null;state=tween=bounds=exitGoal=null
249
- pressed=false;settled=true;boundsDirty=hitDirty=false
303
+ hovered=focused=target=null;state=tween=bounds=exitGoal=retiringBounds=null
304
+ imageRetiring=false
305
+ pressed=false;releasePending=false;settled=true;boundsDirty=hitDirty=false
250
306
  resizeObserver?.disconnect();window.cancelAnimationFrame(raf);raf=0
251
307
  frame.style.opacity='0';frame.removeAttribute('data-pressed')
252
308
  }
309
+ const syncModal=()=>{
310
+ const modals=[...document.querySelectorAll('dialog[open],[role="dialog"][aria-modal="true"]')]
311
+ const previousModal=activeModal
312
+ activeModal=modals.filter(node=>!node.hidden && node.getAttribute('aria-hidden')!=='true').at(-1) ?? null
313
+ if(activeModal===previousModal)return
314
+ if(activeModal && target && !activeModal.contains(target) && target.querySelector('img') && activeModal.querySelector('img') && releasePending && bounds && !reduced?.matches){
315
+ // The preview is opaque: show a bounded click acknowledgment above it,
316
+ // then fade at the thumbnail position instead of retaining a hover target.
317
+ imageRetiring=true;motionDuration=120;tween=null
318
+ retiringBounds={left:bounds.left,top:bounds.top,width:bounds.width,height:bounds.height}
319
+ target.removeAttribute('data-prts-corner-target');hovered=focused=target=null
320
+ boundsDirty=hitDirty=false;resizeObserver?.disconnect();settled=false;wake()
321
+ }else if(imageRetiring || activeModal && (!target || !activeModal.contains(target)))reset()
322
+ }
323
+ // Native image previews mount a body portal without moving the pointer.
324
+ // Watch portal changes only, not the streaming conversation subtree.
325
+ const modalObserver=window.MutationObserver?new window.MutationObserver(syncModal):null
326
+ modalObserver?.observe(document.body,{childList:true})
327
+ const modalToggle=event=>{if(event.target?.matches?.('dialog'))syncModal()}
328
+ document.addEventListener('toggle',modalToggle,true)
329
+ syncModal()
253
330
  const visibility=()=>{if(document.hidden)reset()}
254
331
  const scroll=()=>{if(!target && !state?.alpha)return;boundsDirty=true;hitDirty=!!hovered;wake()}
255
332
  document.addEventListener('scroll',scroll,true)
256
333
  window.addEventListener('resize',reset)
257
- const key=event=>{if(target && (event.key===' ' || event.key==='Enter')){pressed=event.type==='keydown';motionDuration=pressed?100:140;settled=false;wake()}}
334
+ const key=event=>{if(target && (event.key===' ' || event.key==='Enter')){if(event.type==='keyup')release();else {pressed=true;releasePending=false;motionDuration=100;settled=false;wake()}}}
258
335
  const listeners=[
259
336
  [document,'pointermove',move],[document,'pointerleave',leave],
260
337
  [document,'focusin',focus],[document,'focusout',blur],
@@ -264,6 +341,8 @@ function mountBodyTargetFrame(document, window, operation) {
264
341
  ]
265
342
  for(const [node,event,handler] of listeners)node.addEventListener(event,handler)
266
343
  return ()=>{
344
+ clearMove()
345
+ modalObserver?.disconnect();document.removeEventListener('toggle',modalToggle,true)
267
346
  disposed=true;target?.removeAttribute('data-prts-corner-target');hideFrame();window.cancelAnimationFrame(raf);resizeObserver?.disconnect()
268
347
  document.removeEventListener('scroll',scroll,true);window.removeEventListener('resize',reset)
269
348
  for(const [node,event,handler] of listeners)node.removeEventListener(event,handler)
@@ -8,7 +8,8 @@ export function createCursorAdapter({ document, window, assets }) {
8
8
  let gestureAssets = assets
9
9
  let textTarget
10
10
  let lastPoint
11
- const contentSelector = '[data-markdown], [data-slot*="markdown" i], [class*="markdown" i], [data-chat-flow-kind="user"] [class$="_bubble"]'
11
+ const heroTextSelector = '[data-phase="hero"] [class$="_titleGroup"]'
12
+ const contentSelector = '[data-markdown], [data-slot*="markdown" i], [class*="markdown" i], :is([data-chat-flow-kind="user"], [data-chat-flow-kind="steering"], [data-submission-echo], [data-pending-steering]) [class$="_bubble"]'
12
13
  const controlSelector = 'a[href], button, summary, input, textarea, select, [contenteditable], [role="button"], [role="toolbar"], [role="tab"]'
13
14
  function clearTextHit() {
14
15
  textTarget?.removeAttribute('data-prts-text-hit')
@@ -25,8 +26,8 @@ export function createCursorAdapter({ document, window, assets }) {
25
26
  if (event.pointerType !== 'mouse') return
26
27
  lastPoint = { clientX: event.clientX, clientY: event.clientY, pointerType: 'mouse' }
27
28
  const target = event.target?.nodeType === 1 ? event.target : event.target?.parentElement
28
- const content = target?.closest?.(contentSelector)
29
- if (!content?.closest('[data-chat-flow-kind], [data-message-role]') || target.closest(controlSelector)) return
29
+ const content = target?.closest?.(`${contentSelector}, ${heroTextSelector}`)
30
+ if (!content || (!content.matches(heroTextSelector) && !content.closest('[data-chat-flow-kind], [data-message-role], [data-submission-echo], [data-pending-steering]')) || target.closest(controlSelector)) return
30
31
  const style = window.getComputedStyle(target)
31
32
  if (style.userSelect === 'none' || !style.cursor.includes('data:image/png')) return
32
33
  // Caret APIs return the nearest insertion point even over empty space.
@@ -43,7 +43,7 @@ export function createAvatarStore({window,document}){
43
43
  const focus=()=>{if(window.fetch)void refresh().catch(()=>{})}
44
44
  window.addEventListener('focus',focus)
45
45
  return{
46
- snapshot:()=>state,refresh,
46
+ snapshot:()=>state,refresh,applySnapshot:apply,
47
47
  subscribe(fn){listeners.add(fn);fn(state);return()=>listeners.delete(fn)},
48
48
  async edit(role){
49
49
  const response=await window.fetch(API+'/edit/'+role,{credentials:'same-origin',signal:controller?.signal})
@@ -17,7 +17,7 @@ export function createHostGeometryAdapter({ document, window }) {
17
17
  const root = document?.documentElement
18
18
  let frame
19
19
  let sidebar
20
- let previousSidebarWidth
20
+ const sidebarWidths = new Map()
21
21
  let resizeObserver
22
22
  let frameRequest
23
23
  let resizeSettleTimer
@@ -35,7 +35,11 @@ export function createHostGeometryAdapter({ document, window }) {
35
35
  }
36
36
  if (sidebar?.isConnected) {
37
37
  const width = cssPixels(sidebar.getBoundingClientRect().width)
38
- if (root.style.getPropertyValue('--prts-sidebar-width') !== width) root.style.setProperty('--prts-sidebar-width', width)
38
+ for (const node of [sidebar, document.querySelector('[data-prts-nav-rail]')]) {
39
+ if (!node) continue
40
+ if (!sidebarWidths.has(node)) sidebarWidths.set(node, [node.style.getPropertyValue('--prts-sidebar-width'), node.style.getPropertyPriority('--prts-sidebar-width')])
41
+ if (node.style.getPropertyValue('--prts-sidebar-width') !== width) node.style.setProperty('--prts-sidebar-width', width)
42
+ }
39
43
  }
40
44
  return top
41
45
  }
@@ -103,8 +107,11 @@ export function createHostGeometryAdapter({ document, window }) {
103
107
  else root.style.removeProperty(TOP_INSET_PROPERTY)
104
108
  previousValue = undefined
105
109
  previousPriority = undefined
106
- if (previousSidebarWidth) root.style.setProperty('--prts-sidebar-width', previousSidebarWidth)
107
- else root.style.removeProperty('--prts-sidebar-width')
110
+ for (const [node, [value, priority]] of sidebarWidths) {
111
+ if (value) node.style.setProperty('--prts-sidebar-width', value, priority)
112
+ else node.style.removeProperty('--prts-sidebar-width')
113
+ }
114
+ sidebarWidths.clear()
108
115
  sidebar = undefined
109
116
  frame = undefined
110
117
  observedFrameHeight = undefined
@@ -121,7 +128,6 @@ export function createHostGeometryAdapter({ document, window }) {
121
128
  if (started) dispose()
122
129
  frame = nextFrame
123
130
  sidebar = frame.querySelector?.('[data-prts-region="sessions"]')
124
- previousSidebarWidth = root.style.getPropertyValue('--prts-sidebar-width')
125
131
  previousValue = root.style.getPropertyValue(TOP_INSET_PROPERTY)
126
132
  previousPriority = root.style.getPropertyPriority(TOP_INSET_PROPERTY)
127
133
  started = true
@@ -208,6 +208,7 @@ export function applyPrtsPlugin(ctx, environment) {
208
208
  document, window, assets, adapter, hostGeometry, customEmblemStore, avatarStore, backgroundStore,
209
209
  themeTransfer: themeTransfer ||= createThemeTransfer({ window, getPreferences: () => nativePreferences.value,
210
210
  savePreferences: (value, revision) => nativePreferences.replace(value, revision),
211
+ onAvatarConfiguration: value => avatarStore.applySnapshot(value),
211
212
  checkWritable: () => nativePreferences.checkWritable(), checkExport: () => nativePreferences.checkReadable(), themeVersion: version }),
212
213
  onSchemeToggle(next, interaction) {
213
214
  return theme.setTheme(next, { ...interaction, animate: true })
@@ -222,6 +223,7 @@ export function applyPrtsPlugin(ctx, environment) {
222
223
  persistAndApply(updatePreferenceValue(preferences, key, value))
223
224
  },
224
225
  onPreferencePreview(key, value) {
226
+ if (key === 'cornerColor') { document.documentElement.style.setProperty('--prts-corner-color', String(value)); return }
225
227
  if (key === 'particleTraversalSpeed') {
226
228
  particleField.update(updatePreferenceValue(preferences, key, value))
227
229
  return
@@ -39,6 +39,8 @@ export const DEFAULT_PREFERENCES = Object.freeze({
39
39
  bakerBlur: 8,
40
40
  classicUserBlur: 8,
41
41
  cursor: 'prts',
42
+ cornerColor: '#f0c800',
43
+ cornerCustomColor: '#f0c800',
42
44
  conversationParticleDensity: 'sparse',
43
45
  heroParticleDensity: 'light',
44
46
  particleTraversalSpeed: PARTICLE_TRAVERSAL_SPEED_DEFAULT,
@@ -105,7 +107,7 @@ export const PREFERENCE_GROUPS = Object.freeze({
105
107
  background: Object.freeze(['texture', 'backgroundMode', 'backgroundDim', 'heroContrast', 'sidebarOpacity', 'rightSidebarOpacity', 'railOpacity', 'bakerOpacity', 'classicUserOpacity', 'sidebarBlur', 'rightSidebarBlur', 'railBlur', 'bakerBlur', 'classicUserBlur']),
106
108
  particles: Object.freeze(['conversationParticleDensity', 'heroParticleDensity', 'particleTraversalSpeed']),
107
109
  material: Object.freeze(['glass', 'composerGlass']),
108
- accessibility: Object.freeze(['motion', 'bootAnimation', 'cursor']),
110
+ accessibility: Object.freeze(['motion', 'bootAnimation', 'cursor', 'cornerColor', 'cornerCustomColor']),
109
111
  aiTools: Object.freeze(['aiToolsEnabled']),
110
112
  navigation: Object.freeze(['railDefaultHidden', 'sessionPreview', 'previewOngoing', 'previewTools', 'previewWarning', 'previewDone', 'previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning', 'previewReplyLimit']),
111
113
  conversation: Object.freeze(['sessionFlow', 'sessionGlow', 'sessionFlowPalette', 'sessionFlowColorBack', 'sessionFlowColorCore', 'sessionFlowColorFront', 'sessionFlowSpeed', 'conversationStyle', 'bakerGroupAvatars', 'bakerLoading', 'conversationEmblem']),
@@ -229,6 +231,8 @@ export function normalizePreferences(value) {
229
231
  backgroundDim: typeof input.backgroundDim === 'number' && Number.isFinite(input.backgroundDim) ? Math.min(.9, Math.max(0, input.backgroundDim)) : .45,
230
232
  composerGlass: booleanOr(input.composerGlass, DEFAULT_PREFERENCES.composerGlass),
231
233
  bootAnimation: booleanOr(input.bootAnimation, DEFAULT_PREFERENCES.bootAnimation),
234
+ cornerCustomColor: colorOr(input.cornerCustomColor, DEFAULT_PREFERENCES.cornerCustomColor),
235
+ cornerColor: colorOr(input.cornerColor, DEFAULT_PREFERENCES.cornerColor),
232
236
  cursor: input.cursor === 'system' ? 'system' : DEFAULT_PREFERENCES.cursor,
233
237
  conversationParticleDensity: enumOr(legacyDensities.conversation, PARTICLE_DENSITIES, particlePreset.conversationParticleDensity),
234
238
  heroParticleDensity: enumOr(legacyDensities.hero, PARTICLE_DENSITIES, particlePreset.heroParticleDensity),
@@ -283,6 +287,10 @@ export function updatePreferenceValue(value, key, nextValue) {
283
287
  if (key === 'preset') return applyVisualPreset(value, nextValue)
284
288
  if (key === 'particleDetail') return applyParticleDetail(value, nextValue)
285
289
  const current = normalizePreferences(value)
290
+ if (key === 'cornerCustomColor') {
291
+ const color = colorOr(nextValue, current.cornerCustomColor)
292
+ return normalizePreferences({ ...current, cornerColor: color, cornerCustomColor: color })
293
+ }
286
294
  if (key === 'sessionFlowPalette') {
287
295
  const palette = enumOr(nextValue, SESSION_FLOW_PALETTE_IDS, current.sessionFlowPalette)
288
296
  const colors = SESSION_FLOW_PALETTES[palette]
@@ -95,9 +95,13 @@ function visibleSessionSummaries(sessions, workspaces, sessionStatus) {
95
95
  })
96
96
  }
97
97
 
98
+ function setSidebarAttribute(node, attribute, value) {
99
+ if (node.getAttribute(attribute) !== value) node.setAttribute(attribute, value)
100
+ }
101
+
98
102
  function markSidebarNode(node, attribute, next) {
99
103
  if (!node) return
100
- node.setAttribute(attribute, '')
104
+ setSidebarAttribute(node, attribute, '')
101
105
  next.add(node)
102
106
  }
103
107
 
@@ -174,9 +178,9 @@ function ensureFacilityFace(document, row, kind, next) {
174
178
  face.setAttribute('data-prts-owned-facility', '')
175
179
  row.insertBefore(face, row.firstChild)
176
180
  }
177
- face.setAttribute('data-prts-facility-face', kind)
178
- face.setAttribute('data-prts-facility-texture', kind === 'workspace' ? 'silhouette' : 'pickup')
179
- face.setAttribute('aria-hidden', 'true')
181
+ setSidebarAttribute(face, 'data-prts-facility-face', kind)
182
+ setSidebarAttribute(face, 'data-prts-facility-texture', kind === 'workspace' ? 'silhouette' : 'pickup')
183
+ setSidebarAttribute(face, 'aria-hidden', 'true')
180
184
  next.add(face)
181
185
  return face
182
186
  }
@@ -188,8 +192,8 @@ function ensureFacilitySpine(document, row, kind, next) {
188
192
  spine.setAttribute('data-prts-owned-facility-spine', '')
189
193
  row.insertBefore(spine, row.firstChild)
190
194
  }
191
- spine.setAttribute('data-prts-facility-spine', kind)
192
- spine.setAttribute('aria-hidden', 'true')
195
+ setSidebarAttribute(spine, 'data-prts-facility-spine', kind)
196
+ setSidebarAttribute(spine, 'aria-hidden', 'true')
193
197
  next.add(spine)
194
198
  return spine
195
199
  }
@@ -223,6 +227,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
223
227
  let statusSourcesStarted = false
224
228
  let summaryRoot
225
229
  let summarySignature = ''
230
+ let sourceSummarySignature = ''
226
231
  let heldPreviewState
227
232
  let hoveredSummary, summaryLeaveTimer
228
233
  let summaryDismissed = false, pointerLeftSummary = false
@@ -327,18 +332,26 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
327
332
  sourceRetryTimer = undefined
328
333
  }
329
334
  }
335
+ function onSummarySourceChange() {
336
+ // Streaming activity often republishes unchanged title/status summaries.
337
+ // Native DOM mutations still handle row mounts, selection and hierarchy.
338
+ const signature = JSON.stringify(visibleSessionSummaries(sessions, workspaces, sessionStatus))
339
+ if (signature === sourceSummarySignature && !pendingReveal) return
340
+ sourceSummarySignature = signature
341
+ queueScan()
342
+ }
330
343
  function connectSummarySources() {
331
344
  if (getSessions) sessions = getSessions()
332
345
  if (getWorkspaces) workspaces = getWorkspaces()
333
346
  if (subscribedSessions !== sessions?.list) {
334
347
  stopSessionList()
335
348
  subscribedSessions = sessions?.list
336
- stopSessionList = subscribedSessions?.subscribe?.(queueScan) ?? (() => {})
349
+ stopSessionList = subscribedSessions?.subscribe?.(onSummarySourceChange) ?? (() => {})
337
350
  }
338
351
  if (subscribedWorkspaces !== workspaces?.list) {
339
352
  stopWorkspaceList()
340
353
  subscribedWorkspaces = workspaces?.list
341
- stopWorkspaceList = subscribedWorkspaces?.subscribe?.(queueScan) ?? (() => {})
354
+ stopWorkspaceList = subscribedWorkspaces?.subscribe?.(onSummarySourceChange) ?? (() => {})
342
355
  }
343
356
  }
344
357
  function connectSessionStatus() {
@@ -346,7 +359,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
346
359
  if (source === sessionStatus) return
347
360
  stopSessionStatus()
348
361
  sessionStatus = source
349
- stopSessionStatus = source?.subscribe?.(queueScan) ?? (() => {})
362
+ stopSessionStatus = source?.subscribe?.(onSummarySourceChange) ?? (() => {})
350
363
  }
351
364
 
352
365
  let selectionTarget, selectionTargetTimer
@@ -849,6 +862,28 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
849
862
  }
850
863
 
851
864
 
865
+ let pickupRow, pickupTimer, pickupRoot
866
+ function clearPickup() {
867
+ if (pickupTimer !== undefined) window.clearTimeout(pickupTimer)
868
+ pickupTimer = undefined
869
+ pickupRow?.removeAttribute('data-prts-pickup-ready')
870
+ pickupRow = undefined
871
+ }
872
+ function pickupOver(event) {
873
+ const row = event.target.closest?.('[data-prts-session-row]')
874
+ if (row === pickupRow) return
875
+ clearPickup()
876
+ if (!row) return
877
+ pickupRow = row
878
+ pickupTimer = window.setTimeout(() => {
879
+ pickupTimer = undefined
880
+ if (row.isConnected && pickupRow === row) row.setAttribute('data-prts-pickup-ready', '')
881
+ }, 100)
882
+ }
883
+ function pickupOut(event) {
884
+ if (pickupRow && !pickupRow.contains(event.relatedTarget)) clearPickup()
885
+ }
886
+
852
887
  function queueScan() {
853
888
  if (!adapterActive || scanFrame !== undefined) return
854
889
  const requestFrame = window?.requestAnimationFrame?.bind(window) ?? (callback => window?.setTimeout?.(callback, 0))
@@ -861,6 +896,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
861
896
  function scan() {
862
897
  if (!adapterActive) return
863
898
  syncSummarySources()
899
+ sourceSummarySignature = JSON.stringify(visibleSessionSummaries(sessions, workspaces, sessionStatus))
864
900
  const root = document?.querySelector?.('[data-prts-region="sessions"] [data-slot="sidebar.workspaces"]')
865
901
  const next = new Set()
866
902
  const nextVectorOwners = new Set()
@@ -919,16 +955,16 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
919
955
  }
920
956
  markSidebarNode(row, 'data-prts-session-row', next)
921
957
  if (state) {
922
- row.setAttribute('data-prts-session-state', state)
958
+ setSidebarAttribute(row, 'data-prts-session-state', state)
923
959
  next.add(row)
924
960
  } else {
925
961
  row.removeAttribute('data-prts-session-state')
926
962
  }
927
- row.dataset.prtsSessionIndex = sessionIndex
963
+ setSidebarAttribute(row, 'data-prts-session-index', sessionIndex)
928
964
  const title = resolveRowTitle(row, 'session', controls.actions)
929
965
  markSidebarNode(title, 'data-prts-row-title', next)
930
966
  markSidebarNode(resolveSessionTime(row, title, controls.actions), 'data-prts-session-time', next)
931
- if (title) title.dataset.prtsSessionIndex = sessionIndex
967
+ if (title) setSidebarAttribute(title, 'data-prts-session-index', sessionIndex)
932
968
  row.toggleAttribute('data-prts-row-menu-open', controls.menu?.getAttribute('aria-expanded') === 'true'
933
969
  || row.classList.contains('menuOpen'))
934
970
  markSidebarNode(controls.actions, 'data-prts-session-actions', next)
@@ -1007,8 +1043,18 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
1007
1043
  if (observer || !window?.MutationObserver) return
1008
1044
  const observationRoot = document.querySelector('[data-prts-region="sessions"]')
1009
1045
  if (!observationRoot) return
1046
+ pickupRoot = observationRoot
1047
+ pickupRoot.addEventListener('pointerover', pickupOver)
1048
+ pickupRoot.addEventListener('pointerout', pickupOut)
1010
1049
  observer = new window.MutationObserver(mutations => {
1011
- const relevant = mutations.filter(mutation => !mutation.target.closest?.('[data-prts-owned-session-summary]'))
1050
+ const relevant = mutations.filter(mutation => {
1051
+ if (mutation.target.closest?.('[data-prts-owned-session-summary]')) return false
1052
+ if (mutation.type === 'attributes' && mutation.attributeName === 'class' && !mutation.target.closest('[role="treeitem"]')) {
1053
+ const stableClasses = value => (value ?? '').split(/\s+/).filter(token => token && !token.endsWith('_quietBars')).sort().join(' ')
1054
+ if (stableClasses(mutation.oldValue) === stableClasses(mutation.target.getAttribute('class'))) return false
1055
+ }
1056
+ return true
1057
+ })
1012
1058
  const freshRow = 'div[role="treeitem"]:not([data-prts-session-row]):not([data-prts-workspace-row])'
1013
1059
  const mountsRows = relevant.some(mutation => mutation.type === 'childList'
1014
1060
  && Array.from(mutation.addedNodes).some(node => node.nodeType === 1
@@ -1026,9 +1072,14 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
1026
1072
  childList: true,
1027
1073
  attributes: true,
1028
1074
  attributeFilter: ['aria-expanded', 'aria-selected', 'class', 'data-state'],
1075
+ attributeOldValue: true,
1029
1076
  })
1030
1077
  },
1031
1078
  dispose() {
1079
+ clearPickup()
1080
+ pickupRoot?.removeEventListener('pointerover', pickupOver)
1081
+ pickupRoot?.removeEventListener('pointerout', pickupOut)
1082
+ pickupRoot = undefined
1032
1083
  adapterActive = false
1033
1084
  disconnectSummarySources()
1034
1085
  cancelReveal()
@@ -19,6 +19,7 @@ const ROOT_ATTRIBUTES = [
19
19
  'data-prts-background-mode',
20
20
  ]
21
21
  const SESSION_FLOW_COLOR_PROPERTIES = {
22
+ '--prts-corner-color': 'cornerColor',
22
23
  '--prts-session-flow-back': 'sessionFlowColorBack',
23
24
  '--prts-session-flow-core': 'sessionFlowColorCore',
24
25
  '--prts-session-flow-front': 'sessionFlowColorFront',
@@ -14,6 +14,7 @@ const ROOT_ATTRIBUTES = [
14
14
  'data-prts-conversation-style',
15
15
  ]
16
16
  const SESSION_FLOW_COLOR_PROPERTIES = {
17
+ '--prts-corner-color': 'cornerColor',
17
18
  '--prts-session-flow-back': 'sessionFlowColorBack',
18
19
  '--prts-session-flow-core': 'sessionFlowColorCore',
19
20
  '--prts-session-flow-front': 'sessionFlowColorFront',