dsh-smooth-stream 0.5.0 → 0.6.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/README.en.md +4 -1
- package/README.md +5 -1
- package/lib/client.js +572 -93
- package/lib/client.js.map +1 -1
- package/lib/index.js +219 -1
- package/package.json +1 -1
- package/src/client/LogarithmicFade.module.css +160 -0
- package/src/client/SmoothStreamCard.module.css +34 -0
- package/src/client/SmoothStreamCard.tsx +42 -0
- package/src/client/TypewriterAssistantNodeView.tsx +131 -18
- package/src/client/index.ts +4 -0
- package/src/client/locales.ts +25 -0
- package/src/client/smooth-stream-card-controller.ts +10 -3
- package/src/client/smooth-stream-settings-api.ts +7 -3
- package/src/client/teleprompterGlide.ts +8 -18
- package/src/client/turn-process-face.d.ts +27 -0
- package/src/client/useLogarithmicFade.ts +293 -0
- package/src/plugin.ts +39 -4
- package/src/settings-api.ts +4 -0
- package/src/settings-channel.ts +277 -0
- package/src/settings.ts +23 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { memo, useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentProps, type ReactNode } from 'react'
|
|
1
|
+
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentProps, type ReactNode, type RefObject } from 'react'
|
|
2
2
|
import { IconThinkOutline14, JsonBlock, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
3
3
|
import { ImageGallery, type ImageLoader, type MessageImageLabels } from '@deepseek-ai/dsh-client-ui-attachment'
|
|
4
4
|
import type { ChatNodeViewProps, TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
@@ -6,9 +6,10 @@ import { AnimatedDisclosure } from './AnimatedDisclosure.tsx'
|
|
|
6
6
|
import { notifyFollowCommit } from './teleprompterGlide.ts'
|
|
7
7
|
import { useSmoothStreamContent, type StreamSmoothingPreset } from './useSmoothStreamContent.ts'
|
|
8
8
|
import { useFpsGuard } from './useFpsGuard.ts'
|
|
9
|
+
import { useLogarithmicFade } from './useLogarithmicFade.ts'
|
|
9
10
|
import { FollowHost } from './FollowHost.tsx'
|
|
10
11
|
import { DEFAULT_STREAM_CONFIG, type StreamMode } from '../config.ts'
|
|
11
|
-
import { DEFAULT_STREAM_SETTINGS } from '../settings.ts'
|
|
12
|
+
import { DEFAULT_STREAM_SETTINGS, type StreamMotionPreference } from '../settings.ts'
|
|
12
13
|
import css from './TypewriterAssistantNodeView.module.css'
|
|
13
14
|
|
|
14
15
|
type AssistantProps = ChatNodeViewProps<'assistant-step'>
|
|
@@ -28,8 +29,26 @@ function usePrefersReducedMotion(): boolean {
|
|
|
28
29
|
return reduced
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Resolve whether the reveal engine should stay off for this view. The OS
|
|
34
|
+
* preference wins only in `auto` mode: `force-smooth` keeps the engine on
|
|
35
|
+
* machines where a system-wide reduce-motion switch (or a forced browser
|
|
36
|
+
* flag) would otherwise silently bypass smoothing, and `force-reduced`
|
|
37
|
+
* disables it even when the OS asks for motion. Credit: three-state design
|
|
38
|
+
* proposed by @Zn-Dk in #21/#22.
|
|
39
|
+
*/
|
|
40
|
+
function useMotionReduced(preference: StreamMotionPreference): boolean {
|
|
41
|
+
const system = usePrefersReducedMotion()
|
|
42
|
+
if (preference === 'force-smooth') return false
|
|
43
|
+
if (preference === 'force-reduced') return true
|
|
44
|
+
return system
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
interface AnimatedMarkdownTextProps extends MarkdownProps {
|
|
32
48
|
streaming: boolean
|
|
49
|
+
logarithmicFade: boolean
|
|
50
|
+
/** Whether the resolved reduced-motion gate keeps the reveal engine off. */
|
|
51
|
+
motionReduced: boolean
|
|
33
52
|
/** True on the last text block: that block owns conversation follow. */
|
|
34
53
|
ownFollow: boolean
|
|
35
54
|
followSpeedCpsRef?: { current: number } | undefined
|
|
@@ -290,6 +309,8 @@ function AnimatedMarkdownText({
|
|
|
290
309
|
labels,
|
|
291
310
|
fileMentions,
|
|
292
311
|
streaming,
|
|
312
|
+
logarithmicFade,
|
|
313
|
+
motionReduced,
|
|
293
314
|
ownFollow,
|
|
294
315
|
followSpeedCpsRef,
|
|
295
316
|
followRevealedCharsRef,
|
|
@@ -299,7 +320,7 @@ function AnimatedMarkdownText({
|
|
|
299
320
|
shouldHoldBack,
|
|
300
321
|
controlScroll = true,
|
|
301
322
|
}: AnimatedMarkdownTextProps) {
|
|
302
|
-
const reduced =
|
|
323
|
+
const reduced = motionReduced
|
|
303
324
|
const [typing, setTyping] = useState(streaming)
|
|
304
325
|
const localSpeedCpsRef = useRef(35)
|
|
305
326
|
const followRootRef = useRef<HTMLDivElement>(null)
|
|
@@ -319,6 +340,7 @@ function AnimatedMarkdownText({
|
|
|
319
340
|
})
|
|
320
341
|
const shown = reduced ? text : displayed
|
|
321
342
|
const live = typing && !reduced
|
|
343
|
+
useLogarithmicFade(followRootRef, logarithmicFade && !reduced, live, speedCpsRef)
|
|
322
344
|
|
|
323
345
|
useEffect(() => {
|
|
324
346
|
const root = followRootRef.current
|
|
@@ -350,6 +372,14 @@ function AnimatedMarkdownText({
|
|
|
350
372
|
if (typing && !streaming && shown.length === text.length) setTyping(false)
|
|
351
373
|
}, [shown, streaming, text, typing])
|
|
352
374
|
|
|
375
|
+
// A row can mount before the projection flips its assistant step to
|
|
376
|
+
// `running`, which would freeze `typing` at false forever and leave the
|
|
377
|
+
// reveal engine off for the whole reply. Re-arm on the rising edge so late
|
|
378
|
+
// stream starts are still smoothed. (Adopted from #22 by @Zn-Dk.)
|
|
379
|
+
useEffect(() => {
|
|
380
|
+
if (streaming) setTyping(true)
|
|
381
|
+
}, [streaming])
|
|
382
|
+
|
|
353
383
|
return (
|
|
354
384
|
<FollowHost
|
|
355
385
|
active={live && ownFollow}
|
|
@@ -387,6 +417,61 @@ function imageLabels(t: AssistantProps['t']): MessageImageLabels {
|
|
|
387
417
|
}
|
|
388
418
|
}
|
|
389
419
|
|
|
420
|
+
/**
|
|
421
|
+
* Apply searchable hidden state without unmounting a stable subtree — the
|
|
422
|
+
* Host's completion fold hides the answer-inline reasoning this way, so the
|
|
423
|
+
* takeover renderer must reproduce it to stay inside the contract: the row
|
|
424
|
+
* disappears into the Host's process summary and comes back on find-in-page.
|
|
425
|
+
* (Mirrors the Harness chat kit's `useSearchableHidden`.)
|
|
426
|
+
*/
|
|
427
|
+
function useSearchableHidden(
|
|
428
|
+
hidden: boolean,
|
|
429
|
+
reveal: () => void,
|
|
430
|
+
): RefObject<HTMLDivElement> {
|
|
431
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
432
|
+
useLayoutEffect(() => {
|
|
433
|
+
const element = ref.current
|
|
434
|
+
if (element === null) return
|
|
435
|
+
if (hidden && element.contains(element.ownerDocument.activeElement)) {
|
|
436
|
+
reveal()
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
if (hidden) element.setAttribute('hidden', 'until-found')
|
|
440
|
+
else element.removeAttribute('hidden')
|
|
441
|
+
}, [hidden, reveal])
|
|
442
|
+
useEffect(() => {
|
|
443
|
+
const element = ref.current
|
|
444
|
+
if (element === null) return
|
|
445
|
+
element.addEventListener('beforematch', reveal)
|
|
446
|
+
return () => { element.removeEventListener('beforematch', reveal) }
|
|
447
|
+
}, [reveal])
|
|
448
|
+
return ref
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* One answer-inline reasoning block wrapped in the Host's fold contract. The
|
|
453
|
+
* Host computes the fold decision (turn closed, compact transcript, this node
|
|
454
|
+
* is the answer) and delivers it through the `turnProcess` owner prop; when
|
|
455
|
+
* folded the block hides into the Host's "thought" summary row instead of
|
|
456
|
+
* staying mounted above the answer.
|
|
457
|
+
*/
|
|
458
|
+
function FoldableReasoning({
|
|
459
|
+
hidden,
|
|
460
|
+
reveal,
|
|
461
|
+
children,
|
|
462
|
+
}: {
|
|
463
|
+
hidden: boolean
|
|
464
|
+
reveal: () => void
|
|
465
|
+
children: ReactNode
|
|
466
|
+
}) {
|
|
467
|
+
const ref = useSearchableHidden(hidden, reveal)
|
|
468
|
+
return (
|
|
469
|
+
<div ref={ref} data-turn-process-inline={hidden || undefined}>
|
|
470
|
+
{children}
|
|
471
|
+
</div>
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
|
|
390
475
|
function firstLine(text: string): string {
|
|
391
476
|
const newline = text.indexOf('\n')
|
|
392
477
|
return newline === -1 ? text : text.slice(0, newline)
|
|
@@ -412,6 +497,8 @@ function AnimatedReasoning({
|
|
|
412
497
|
running,
|
|
413
498
|
preset,
|
|
414
499
|
thinkAutoExpand,
|
|
500
|
+
motionReduced,
|
|
501
|
+
logarithmicFade,
|
|
415
502
|
shouldHoldBack,
|
|
416
503
|
followSpeedCpsRef,
|
|
417
504
|
followRevealScaleRef,
|
|
@@ -421,15 +508,20 @@ function AnimatedReasoning({
|
|
|
421
508
|
running: boolean
|
|
422
509
|
preset: StreamSmoothingPreset
|
|
423
510
|
thinkAutoExpand: boolean
|
|
511
|
+
motionReduced: boolean
|
|
512
|
+
logarithmicFade: boolean
|
|
424
513
|
shouldHoldBack: () => boolean
|
|
425
514
|
followSpeedCpsRef?: { current: number } | undefined
|
|
426
515
|
followRevealScaleRef?: { current: number } | undefined
|
|
427
516
|
t: AssistantProps['t']
|
|
428
517
|
}) {
|
|
429
|
-
const reduced =
|
|
518
|
+
const reduced = motionReduced
|
|
430
519
|
const [expanded, setExpanded] = useState(running && thinkAutoExpand)
|
|
431
520
|
const [autoClosed, setAutoClosed] = useState(false)
|
|
432
521
|
const summaryRef = useRef<HTMLSpanElement>(null)
|
|
522
|
+
const fadeRootRef = useRef<HTMLDivElement>(null)
|
|
523
|
+
const localFadeSpeedRef = useRef(35)
|
|
524
|
+
const fadeSpeedRef = followSpeedCpsRef ?? localFadeSpeedRef
|
|
433
525
|
const commitAnchorRef = useRef<HTMLDivElement>(null)
|
|
434
526
|
// The running→false flip is the AUTO-close: it collapses instantly and the
|
|
435
527
|
// follower's settle spring absorbs the height step. A later manual toggle
|
|
@@ -438,12 +530,13 @@ function AnimatedReasoning({
|
|
|
438
530
|
enabled: running && !reduced,
|
|
439
531
|
preset,
|
|
440
532
|
shouldHoldBack,
|
|
441
|
-
speedCpsRef:
|
|
533
|
+
speedCpsRef: fadeSpeedRef,
|
|
442
534
|
revealScaleRef: followRevealScaleRef,
|
|
443
535
|
onRevealCommit: () => { notifyFollowCommit(commitAnchorRef.current) },
|
|
444
536
|
})
|
|
445
537
|
const shown = running && !reduced ? displayed : text
|
|
446
538
|
const summary = running ? latestLine(shown) : firstLine(text)
|
|
539
|
+
useLogarithmicFade(fadeRootRef, logarithmicFade && !reduced && expanded, running, fadeSpeedRef)
|
|
447
540
|
|
|
448
541
|
useLayoutEffect(() => {
|
|
449
542
|
// Only the running state owns disclosure while auto-expand is on; with it
|
|
@@ -493,7 +586,7 @@ function AnimatedReasoning({
|
|
|
493
586
|
</>
|
|
494
587
|
)}
|
|
495
588
|
>
|
|
496
|
-
<div className={css.thinkBody}>{shown}</div>
|
|
589
|
+
<div ref={fadeRootRef} className={css.thinkBody}>{shown}</div>
|
|
497
590
|
</AnimatedDisclosure>
|
|
498
591
|
</div>
|
|
499
592
|
</div>
|
|
@@ -517,12 +610,15 @@ export const TypewriterAssistantNodeView = memo(function TypewriterAssistantNode
|
|
|
517
610
|
scrollSpeedPxPerSec: _scrollSpeedPxPerSec = DEFAULT_STREAM_CONFIG.scrollSpeedPxPerSec,
|
|
518
611
|
maxScrollSpeedPxPerSec: _maxScrollSpeedPxPerSec = DEFAULT_STREAM_CONFIG.maxScrollSpeedPxPerSec,
|
|
519
612
|
thinkAutoExpand = DEFAULT_STREAM_SETTINGS.thinkAutoExpand,
|
|
613
|
+
logarithmicFade = DEFAULT_STREAM_SETTINGS.logarithmicFade,
|
|
520
614
|
controlScroll = true,
|
|
615
|
+
motionPreference = DEFAULT_STREAM_SETTINGS.motionPreference,
|
|
521
616
|
node,
|
|
522
617
|
useTurnData,
|
|
523
618
|
openFile,
|
|
524
619
|
loadImage,
|
|
525
620
|
fileMentions,
|
|
621
|
+
turnProcess,
|
|
526
622
|
t,
|
|
527
623
|
}: AssistantProps & {
|
|
528
624
|
mode?: StreamMode
|
|
@@ -531,11 +627,23 @@ export const TypewriterAssistantNodeView = memo(function TypewriterAssistantNode
|
|
|
531
627
|
scrollSpeedPxPerSec?: number
|
|
532
628
|
maxScrollSpeedPxPerSec?: number
|
|
533
629
|
thinkAutoExpand?: boolean
|
|
630
|
+
logarithmicFade?: boolean
|
|
534
631
|
controlScroll?: boolean
|
|
632
|
+
motionPreference?: StreamMotionPreference
|
|
535
633
|
}) {
|
|
536
634
|
const data = node.data
|
|
537
635
|
const streaming = data.status === 'running'
|
|
538
|
-
const reduced =
|
|
636
|
+
const reduced = useMotionReduced(motionPreference)
|
|
637
|
+
// The Host's completion-fold decision for THIS node's inline reasoning:
|
|
638
|
+
// only the answer step folds, only in compact-transcript mode, and only
|
|
639
|
+
// once the turn has closed (foldable implies it). Mirrors the built-in
|
|
640
|
+
// assistant renderer so the takeover keeps the official behavior.
|
|
641
|
+
const reasoningHidden = turnProcess !== undefined
|
|
642
|
+
&& turnProcess.foldable
|
|
643
|
+
&& turnProcess.spec.answerStep === data.step
|
|
644
|
+
&& turnProcess.spec.inlineReasoning
|
|
645
|
+
&& !turnProcess.open
|
|
646
|
+
const revealProcess = useCallback(() => { turnProcess?.setOpen(true) }, [turnProcess])
|
|
539
647
|
const { ref: guardRef, shouldHoldBack } = useFpsGuard(streaming)
|
|
540
648
|
const rootSpeedRef = useRef(35)
|
|
541
649
|
const rootRevealedCharsRef = useRef(0)
|
|
@@ -608,6 +716,8 @@ export const TypewriterAssistantNodeView = memo(function TypewriterAssistantNode
|
|
|
608
716
|
labels={markdownLabels}
|
|
609
717
|
fileMentions={mentions}
|
|
610
718
|
streaming={streaming}
|
|
719
|
+
logarithmicFade={logarithmicFade && data.status !== 'interrupted'}
|
|
720
|
+
motionReduced={reduced}
|
|
611
721
|
ownFollow={!streaming && index === lastFollow}
|
|
612
722
|
followSpeedCpsRef={index === lastFollow ? rootSpeedRef : undefined}
|
|
613
723
|
followRevealedCharsRef={index === lastFollow ? rootRevealedCharsRef : undefined}
|
|
@@ -621,17 +731,20 @@ export const TypewriterAssistantNodeView = memo(function TypewriterAssistantNode
|
|
|
621
731
|
break
|
|
622
732
|
case 'reasoning':
|
|
623
733
|
rendered.push(
|
|
624
|
-
<
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
734
|
+
<FoldableReasoning key={index} hidden={reasoningHidden} reveal={revealProcess}>
|
|
735
|
+
<AnimatedReasoning
|
|
736
|
+
text={block.text}
|
|
737
|
+
running={streaming && index === last}
|
|
738
|
+
preset={preset}
|
|
739
|
+
thinkAutoExpand={thinkAutoExpand}
|
|
740
|
+
logarithmicFade={logarithmicFade && data.status !== 'interrupted'}
|
|
741
|
+
motionReduced={reduced}
|
|
742
|
+
shouldHoldBack={shouldHoldBack}
|
|
743
|
+
followSpeedCpsRef={reasoningOwnsSpeed && index === last ? rootSpeedRef : undefined}
|
|
744
|
+
followRevealScaleRef={reasoningOwnsSpeed && index === last ? rootRevealScaleRef : undefined}
|
|
745
|
+
t={t}
|
|
746
|
+
/>
|
|
747
|
+
</FoldableReasoning>,
|
|
635
748
|
)
|
|
636
749
|
break
|
|
637
750
|
case 'image': {
|
package/src/client/index.ts
CHANGED
|
@@ -157,7 +157,9 @@ class SettingsCell {
|
|
|
157
157
|
pending === this.pending
|
|
158
158
|
&& next.enabled === this.value.enabled
|
|
159
159
|
&& next.controlScroll === this.value.controlScroll
|
|
160
|
+
&& next.motionPreference === this.value.motionPreference
|
|
160
161
|
&& next.thinkAutoExpand === this.value.thinkAutoExpand
|
|
162
|
+
&& next.logarithmicFade === this.value.logarithmicFade
|
|
161
163
|
&& next.debugEnabled === this.value.debugEnabled
|
|
162
164
|
&& next.debugTuning === this.value.debugTuning
|
|
163
165
|
) return
|
|
@@ -269,7 +271,9 @@ export function apply(ctx: ClientContext): void {
|
|
|
269
271
|
scrollSpeedPxPerSec: config.scrollSpeedPxPerSec,
|
|
270
272
|
maxScrollSpeedPxPerSec: config.maxScrollSpeedPxPerSec,
|
|
271
273
|
thinkAutoExpand: preferences.thinkAutoExpand,
|
|
274
|
+
logarithmicFade: preferences.logarithmicFade,
|
|
272
275
|
controlScroll: preferences.controlScroll,
|
|
276
|
+
motionPreference: preferences.motionPreference,
|
|
273
277
|
})
|
|
274
278
|
}
|
|
275
279
|
ctx.slots.inject('conversation.chat.node', () => {
|
package/src/client/locales.ts
CHANGED
|
@@ -8,7 +8,12 @@ export type SmoothStreamLocaleKey =
|
|
|
8
8
|
| 'title' | 'description'
|
|
9
9
|
| 'enabled' | 'enabledHint'
|
|
10
10
|
| 'controlScroll' | 'controlScrollHint'
|
|
11
|
+
| 'motionPreference' | 'motionPreferenceHint'
|
|
12
|
+
| 'motionAuto' | 'motionAutoHint'
|
|
13
|
+
| 'motionForceSmooth' | 'motionForceSmoothHint'
|
|
14
|
+
| 'motionForceReduced' | 'motionForceReducedHint'
|
|
11
15
|
| 'thinkAutoExpand' | 'thinkAutoExpandHint'
|
|
16
|
+
| 'logarithmicFade' | 'logarithmicFadeHint'
|
|
12
17
|
| 'debugEnabled' | 'debugEnabledHint' | 'debugUnavailable'
|
|
13
18
|
| 'debugPanelTitle' | 'debugPanelToggle' | 'debugPanelClose' | 'debugGuide'
|
|
14
19
|
| 'debugLive' | 'debugIdle' | 'debugUnsaved'
|
|
@@ -37,7 +42,17 @@ export const en: Record<SmoothStreamLocaleKey, string> = {
|
|
|
37
42
|
enabledHint: 'Let this plugin render and follow streaming replies. Turn off to use the built-in Harness renderer.',
|
|
38
43
|
controlScroll: 'Take over scrolling',
|
|
39
44
|
controlScrollHint: 'On by default; smooth-stream writes the conversation scroll position. Turn off to leave bottom-follow to Harness.',
|
|
45
|
+
motionPreference: 'Motion',
|
|
46
|
+
motionPreferenceHint: 'How streaming responds to the system reduce-motion setting.',
|
|
47
|
+
motionAuto: 'Follow system',
|
|
48
|
+
motionAutoHint: 'Reduced-motion systems get raw text (accessibility first).',
|
|
49
|
+
motionForceSmooth: 'Always smooth',
|
|
50
|
+
motionForceSmoothHint: 'Keep the smoothing engine even when the system asks for reduced motion.',
|
|
51
|
+
motionForceReduced: 'Always raw',
|
|
52
|
+
motionForceReducedHint: 'Render raw text even when the system allows motion.',
|
|
40
53
|
thinkAutoExpand: 'Auto-expand thinking',
|
|
54
|
+
logarithmicFade: 'Logarithmic fade',
|
|
55
|
+
logarithmicFadeHint: 'Fade new answer and expanded thinking text into view. Follows the motion preference.',
|
|
41
56
|
thinkAutoExpandHint: 'Open the thinking block while it streams. Turn off to keep it collapsed.',
|
|
42
57
|
debugEnabled: 'Show render diagnostics',
|
|
43
58
|
debugEnabledHint: 'Show live streaming and scroll metrics on the right side of the chat. Tune values there, then save them here.',
|
|
@@ -117,7 +132,17 @@ export const zh: Record<SmoothStreamLocaleKey, string> = {
|
|
|
117
132
|
enabledHint: '由本插件渲染并跟随流式回复;关闭后使用 Harness 内置渲染。',
|
|
118
133
|
controlScroll: '接管滚动',
|
|
119
134
|
controlScrollHint: '默认开启,由丝滑流式写入会话滚动位置;关闭后贴底滚动交给 Harness。',
|
|
135
|
+
motionPreference: '动效偏好',
|
|
136
|
+
motionPreferenceHint: '流式渲染如何响应系统的「减少动态效果」设置。',
|
|
137
|
+
motionAuto: '跟随系统',
|
|
138
|
+
motionAutoHint: '系统开启减少动态效果时直接呈现原始文本(优先无障碍)。',
|
|
139
|
+
motionForceSmooth: '始终平滑',
|
|
140
|
+
motionForceSmoothHint: '即使系统要求减少动态效果,仍保持平滑流式渲染。',
|
|
141
|
+
motionForceReduced: '始终原始',
|
|
142
|
+
motionForceReducedHint: '即使系统允许动效,也直接呈现原始文本。',
|
|
120
143
|
thinkAutoExpand: '自动展开思考',
|
|
144
|
+
logarithmicFade: '对数淡入',
|
|
145
|
+
logarithmicFadeHint: '让回答正文和展开的思考文字由淡变实,遵循动效偏好。',
|
|
121
146
|
thinkAutoExpandHint: '思考块在流式时自动展开;关闭后保持折叠,可手动展开。',
|
|
122
147
|
debugEnabled: '显示渲染调试面板',
|
|
123
148
|
debugEnabledHint: '在聊天右侧显示流式渲染和滚动的实时参数,可在面板中调节并在这里保存。',
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
DEFAULT_STREAM_DEBUG_TUNING,
|
|
6
6
|
DEFAULT_STREAM_SETTINGS,
|
|
7
7
|
type StreamDebugTuning,
|
|
8
|
+
type StreamMotionPreference,
|
|
8
9
|
type StreamSettings,
|
|
9
10
|
} from '../settings.ts'
|
|
10
11
|
import type {
|
|
@@ -23,7 +24,9 @@ export interface SmoothStreamCardState {
|
|
|
23
24
|
failed: boolean
|
|
24
25
|
enabled: boolean
|
|
25
26
|
controlScroll: boolean
|
|
27
|
+
motionPreference: StreamMotionPreference
|
|
26
28
|
thinkAutoExpand: boolean
|
|
29
|
+
logarithmicFade: boolean
|
|
27
30
|
debugEnabled: boolean
|
|
28
31
|
debugTuning: StreamDebugTuning
|
|
29
32
|
debugAvailable: boolean
|
|
@@ -52,7 +55,7 @@ export class SmoothStreamCardController {
|
|
|
52
55
|
private readonly store = createSnapshotStore<SmoothStreamCardState>(this.projection())
|
|
53
56
|
private loaded: StreamSettingsView | undefined
|
|
54
57
|
private loadedDebug: StreamDebugSettingsView | undefined
|
|
55
|
-
private stagedBase: Pick<StreamSettings, 'enabled' | 'controlScroll' | 'thinkAutoExpand'> | undefined
|
|
58
|
+
private stagedBase: Pick<StreamSettings, 'enabled' | 'controlScroll' | 'motionPreference' | 'thinkAutoExpand' | 'logarithmicFade'> | undefined
|
|
56
59
|
private stagedDebug: Pick<StreamSettings, 'debugEnabled' | 'debugTuning'> | undefined
|
|
57
60
|
private saving = false
|
|
58
61
|
private failed = false
|
|
@@ -90,12 +93,14 @@ export class SmoothStreamCardController {
|
|
|
90
93
|
hooks: { smoothStreamCard: this.store },
|
|
91
94
|
edit: (patch) => {
|
|
92
95
|
if (this.saving) return
|
|
93
|
-
if (patch.enabled !== undefined || patch.controlScroll !== undefined || patch.thinkAutoExpand !== undefined) {
|
|
96
|
+
if (patch.enabled !== undefined || patch.controlScroll !== undefined || patch.motionPreference !== undefined || patch.thinkAutoExpand !== undefined || patch.logarithmicFade !== undefined) {
|
|
94
97
|
this.stagedBase = {
|
|
95
98
|
...this.baseValues(),
|
|
96
99
|
...(patch.enabled === undefined ? {} : { enabled: patch.enabled }),
|
|
97
100
|
...(patch.controlScroll === undefined ? {} : { controlScroll: patch.controlScroll }),
|
|
101
|
+
...(patch.motionPreference === undefined ? {} : { motionPreference: patch.motionPreference }),
|
|
98
102
|
...(patch.thinkAutoExpand === undefined ? {} : { thinkAutoExpand: patch.thinkAutoExpand }),
|
|
103
|
+
...(patch.logarithmicFade === undefined ? {} : { logarithmicFade: patch.logarithmicFade }),
|
|
99
104
|
}
|
|
100
105
|
}
|
|
101
106
|
if (this.loadedDebug !== undefined && (patch.debugEnabled !== undefined || patch.debugTuning !== undefined)) {
|
|
@@ -140,11 +145,13 @@ export class SmoothStreamCardController {
|
|
|
140
145
|
}
|
|
141
146
|
}
|
|
142
147
|
|
|
143
|
-
private baseValues(): Pick<StreamSettings, 'enabled' | 'controlScroll' | 'thinkAutoExpand'> {
|
|
148
|
+
private baseValues(): Pick<StreamSettings, 'enabled' | 'controlScroll' | 'motionPreference' | 'thinkAutoExpand' | 'logarithmicFade'> {
|
|
144
149
|
return this.stagedBase ?? {
|
|
145
150
|
enabled: this.loaded?.enabled ?? DEFAULT_STREAM_SETTINGS.enabled,
|
|
146
151
|
controlScroll: this.loaded?.controlScroll ?? DEFAULT_STREAM_SETTINGS.controlScroll,
|
|
152
|
+
motionPreference: this.loaded?.motionPreference ?? DEFAULT_STREAM_SETTINGS.motionPreference,
|
|
147
153
|
thinkAutoExpand: this.loaded?.thinkAutoExpand ?? DEFAULT_STREAM_SETTINGS.thinkAutoExpand,
|
|
154
|
+
logarithmicFade: this.loaded?.logarithmicFade ?? DEFAULT_STREAM_SETTINGS.logarithmicFade,
|
|
148
155
|
}
|
|
149
156
|
}
|
|
150
157
|
|
|
@@ -24,11 +24,13 @@ function settingsView(value: unknown): StreamSettingsView {
|
|
|
24
24
|
|| typeof data.writable !== 'boolean'
|
|
25
25
|
|| typeof data.enabled !== 'boolean'
|
|
26
26
|
|| typeof data.controlScroll !== 'boolean'
|
|
27
|
+
|| !['auto', 'force-smooth', 'force-reduced'].includes(data.motionPreference as string)
|
|
27
28
|
|| typeof data.thinkAutoExpand !== 'boolean'
|
|
29
|
+
|| (data.logarithmicFade !== undefined && typeof data.logarithmicFade !== 'boolean')
|
|
28
30
|
|| typeof data.canUpgrade !== 'boolean') {
|
|
29
31
|
throw new Error('dsh-smooth-stream: malformed settings response')
|
|
30
32
|
}
|
|
31
|
-
return data as unknown as StreamSettingsView
|
|
33
|
+
return { ...data, logarithmicFade: data.logarithmicFade ?? true } as unknown as StreamSettingsView
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
function upgradeView(value: unknown): StreamUpgradeView {
|
|
@@ -68,7 +70,7 @@ function accepted(result: Awaited<ReturnType<ConnectionHandle['rpc']['call']>>):
|
|
|
68
70
|
/** Narrow client contract consumed by the staged settings-card controller. */
|
|
69
71
|
export interface SmoothStreamSettingsApi {
|
|
70
72
|
read(): Promise<StreamSettingsView>
|
|
71
|
-
write(settings: Pick<StreamSettings, 'enabled' | 'controlScroll' | 'thinkAutoExpand'> & Partial<Pick<StreamSettings, 'debugEnabled' | 'debugTuning'>>): Promise<StreamSettingsView>
|
|
73
|
+
write(settings: Pick<StreamSettings, 'enabled' | 'controlScroll' | 'motionPreference' | 'thinkAutoExpand' | 'logarithmicFade'> & Partial<Pick<StreamSettings, 'debugEnabled' | 'debugTuning'>>): Promise<StreamSettingsView>
|
|
72
74
|
readDebug(): Promise<StreamDebugSettingsView>
|
|
73
75
|
writeDebug(settings: Pick<StreamSettings, 'debugEnabled' | 'debugTuning'>): Promise<StreamDebugSettingsView>
|
|
74
76
|
upgrade(): Promise<StreamUpgradeView>
|
|
@@ -80,14 +82,16 @@ export function createSmoothStreamSettingsApi(connection: ConnectionHandle): Smo
|
|
|
80
82
|
async read(): Promise<StreamSettingsView> {
|
|
81
83
|
return settingsView(accepted(await connection.rpc.call(STREAM_SETTINGS_RPC_CHANNEL, STREAM_SETTINGS_RPC.read, {})))
|
|
82
84
|
},
|
|
83
|
-
async write(settings: Pick<StreamSettings, 'enabled' | 'controlScroll' | 'thinkAutoExpand'> & Partial<Pick<StreamSettings, 'debugEnabled' | 'debugTuning'>>): Promise<StreamSettingsView> {
|
|
85
|
+
async write(settings: Pick<StreamSettings, 'enabled' | 'controlScroll' | 'motionPreference' | 'thinkAutoExpand' | 'logarithmicFade'> & Partial<Pick<StreamSettings, 'debugEnabled' | 'debugTuning'>>): Promise<StreamSettingsView> {
|
|
84
86
|
return settingsView(accepted(await connection.rpc.call(
|
|
85
87
|
STREAM_SETTINGS_RPC_CHANNEL,
|
|
86
88
|
STREAM_SETTINGS_RPC.write,
|
|
87
89
|
{
|
|
88
90
|
enabled: settings.enabled,
|
|
89
91
|
controlScroll: settings.controlScroll,
|
|
92
|
+
motionPreference: settings.motionPreference,
|
|
90
93
|
thinkAutoExpand: settings.thinkAutoExpand,
|
|
94
|
+
logarithmicFade: settings.logarithmicFade,
|
|
91
95
|
...(settings.debugEnabled === undefined || settings.debugTuning === undefined
|
|
92
96
|
? {}
|
|
93
97
|
: { debugEnabled: settings.debugEnabled, debugTuning: settings.debugTuning }),
|
|
@@ -780,8 +780,9 @@ function measureReadingAnchor(port: HTMLElement): { anchor: HTMLElement; index:
|
|
|
780
780
|
return null
|
|
781
781
|
}
|
|
782
782
|
// Host-caused motion only: the engine's own shift glide and floor changes
|
|
783
|
-
// cancel out (renderedΔ = topΔ − shiftΔ +
|
|
784
|
-
const
|
|
783
|
+
// cancel out (renderedΔ = topΔ + scrollΔ − shiftΔ + padΔ).
|
|
784
|
+
const scrollDelta = scrollTop - stored.scrollTop
|
|
785
|
+
const delta = (top - stored.top) + scrollDelta - (shift - stored.shift) + (pad - stored.pad)
|
|
785
786
|
if (stored.element === anchor) {
|
|
786
787
|
// Refresh the baseline to the current rendered position on every
|
|
787
788
|
// measurement: between guard passes the viewport legitimately moves
|
|
@@ -2492,25 +2493,14 @@ export function useConversationFollow(
|
|
|
2492
2493
|
// measure the extent against it):
|
|
2493
2494
|
let settleQuietMs = 0
|
|
2494
2495
|
let settleSig = ''
|
|
2495
|
-
|
|
2496
|
-
0,
|
|
2497
|
-
host.scrollHeight - animatedH - runwayOffsetOf(host),
|
|
2498
|
-
)
|
|
2499
|
-
const currentRunway = runwayOffsetOf(host)
|
|
2500
|
-
const currentShift = Math.abs(currentShiftOf(shiftSurfacesOf(host).at(-1) ?? host))
|
|
2501
|
-
if (
|
|
2502
|
-
!activeRef.current
|
|
2503
|
-
&& lagBeforeCompletionPaint <= FOLLOW_SLACK_PX
|
|
2504
|
-
&& currentRunway <= FOLLOW_SETTLE_EPSILON_PX
|
|
2505
|
-
&& currentShift <= FOLLOW_SETTLE_EPSILON_PX
|
|
2506
|
-
// A live completion pad must retire through the settle's glide first;
|
|
2507
|
-
// fast-finishing here would freeze it in place as visible bottom gap.
|
|
2508
|
-
&& flowPadOf(host) <= FOLLOW_SETTLE_EPSILON_PX
|
|
2509
|
-
) {
|
|
2496
|
+
if (!activeRef.current) {
|
|
2510
2497
|
followTraceUntilMs = Math.max(followTraceUntilMs, performance.now() + 10000)
|
|
2511
2498
|
followTrace('fast-gate', { sh: host.scrollHeight, st: Math.round(host.scrollTop), pad: Math.round(flowPadOf(host)) })
|
|
2512
|
-
|
|
2499
|
+
restoreRunway(host)
|
|
2500
|
+
setFlowPad(host, 0)
|
|
2501
|
+
finishAtNaturalFloor(host, !startedAsEntrance, true)
|
|
2513
2502
|
followLeaders.delete(host)
|
|
2503
|
+
followCompletionSettle.delete(host)
|
|
2514
2504
|
releaseRevealScale()
|
|
2515
2505
|
debugRuntime.reportFollow(host, null)
|
|
2516
2506
|
return
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn-process fold face for the DSH 0.1.2 completion-collapse contract.
|
|
3
|
+
*
|
|
4
|
+
* The Host seat hands every keyed Chat renderer a `turnProcess` owner prop
|
|
5
|
+
* when the node belongs to a projected Turn: the fold decision lives in the
|
|
6
|
+
* conversation snapshot (turn closed + compact transcript), and the renderer
|
|
7
|
+
* is expected to hide its answer-inline reasoning while folded. The pinned
|
|
8
|
+
* type-only dependency predates the field, so the face is declared here and
|
|
9
|
+
* merged into the owner-props interface; erases at compile time.
|
|
10
|
+
*/
|
|
11
|
+
export interface TurnProcessOwnerFace {
|
|
12
|
+
/** Answer anchor of the projection; null while the answer has not landed. */
|
|
13
|
+
readonly spec: { readonly answerStep: number | null, readonly inlineReasoning: boolean }
|
|
14
|
+
/** Whether this node's process content may fold (turn closed + compact mode). */
|
|
15
|
+
readonly foldable: boolean
|
|
16
|
+
/** Current fold state, persisted by the Host across sessions. */
|
|
17
|
+
readonly open: boolean
|
|
18
|
+
/** Fold or unfold from the renderer side (summary click, find-in-page reveal). */
|
|
19
|
+
setOpen(open: boolean): void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare module '@deepseek-ai/dsh-client-ui-conversation/client' {
|
|
23
|
+
interface ChatNodeOwnerProps {
|
|
24
|
+
/** Present only when the node belongs to a projected Turn (0.1.2+). */
|
|
25
|
+
turnProcess?: TurnProcessOwnerFace | undefined
|
|
26
|
+
}
|
|
27
|
+
}
|