dsh-smooth-stream 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +1508 -362
- package/lib/client.js.map +1 -1
- package/lib/index.js +102 -8
- package/package.json +1 -1
- package/src/client/AgentRowEntrance.module.css +1 -4
- package/src/client/AnimatedDisclosure.tsx +3 -3
- package/src/client/DebugPanel.tsx +34 -15
- package/src/client/FrameCoordinator.ts +180 -0
- package/src/client/SmoothStreamCard.tsx +28 -3
- package/src/client/StreamBuffer.ts +145 -0
- package/src/client/TypewriterAssistantNodeView.module.css +82 -12
- package/src/client/TypewriterAssistantNodeView.tsx +216 -102
- package/src/client/debugRuntime.ts +58 -0
- package/src/client/harnessIcons.ts +27 -0
- package/src/client/index.ts +151 -11
- package/src/client/locales.ts +85 -0
- package/src/client/smooth-stream-card-controller.ts +7 -3
- package/src/client/smooth-stream-settings-api.ts +4 -2
- package/src/client/teleprompterGlide.ts +491 -52
- package/src/client/useDecoupledMarkdown.ts +173 -0
- package/src/client/useFpsGuard.ts +9 -6
- package/src/client/useLogarithmicFade.ts +439 -60
- package/src/client/useProgressiveDomText.ts +27 -11
- package/src/client/useSmoothStreamContent.ts +5 -13
- package/src/config.ts +1 -1
- package/src/plugin.ts +83 -8
- package/src/settings-api.ts +2 -0
- package/src/settings-channel.ts +34 -8
- package/src/settings.ts +11 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ComponentType } from 'react'
|
|
2
|
+
import * as primitives from '@deepseek-ai/dsh-client-ui-primitives'
|
|
3
|
+
|
|
4
|
+
type IconProps = { className?: string | undefined; size?: number | undefined }
|
|
5
|
+
type IconExports = Record<string, ComponentType<IconProps> | undefined>
|
|
6
|
+
|
|
7
|
+
// Harness renamed size-suffixed icon exports to weight-suffixed names. Resolve
|
|
8
|
+
// at runtime so this plugin bundle works with both the older published
|
|
9
|
+
// primitive package used for development and current Harness module tables.
|
|
10
|
+
const icons = primitives as unknown as IconExports
|
|
11
|
+
|
|
12
|
+
function resolveIcon(currentName: string, legacyName: string): ComponentType<IconProps> {
|
|
13
|
+
const icon = icons[currentName] ?? icons[legacyName]
|
|
14
|
+
if (icon === undefined) {
|
|
15
|
+
throw new Error(`dsh-smooth-stream: Harness icon export is missing (${currentName}, ${legacyName})`)
|
|
16
|
+
}
|
|
17
|
+
return icon
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const IconChevronDown = resolveIcon('IconChevronDownOutlineRegular', 'IconChevronDownOutline14')
|
|
21
|
+
export const IconClose = resolveIcon('IconCloseOutlineRegular', 'IconCloseOutline16')
|
|
22
|
+
export const IconCode = resolveIcon('IconCodeOutlineRegular', 'IconCodeOutline16')
|
|
23
|
+
export const IconCopy = resolveIcon('IconCopyOutlineRegular', 'IconCopyOutline16')
|
|
24
|
+
export const IconQuestion = resolveIcon('IconQuestionOutlineRegular', 'IconQuestionOutline14')
|
|
25
|
+
export const IconRefresh = resolveIcon('IconRefreshOutlineRegular', 'IconRefreshOutline14')
|
|
26
|
+
export const IconRefreshSmall = resolveIcon('IconRefreshOutlineRegular', 'IconRefreshOutline16')
|
|
27
|
+
export const IconThink = resolveIcon('IconThinkOutlineRegular', 'IconThinkOutline14')
|
package/src/client/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { SmoothStreamCardController } from './smooth-stream-card-controller.ts'
|
|
|
13
13
|
import { createSmoothStreamSettingsApi } from './smooth-stream-settings-api.ts'
|
|
14
14
|
import { DebugPanel } from './DebugPanel.tsx'
|
|
15
15
|
import { debugRuntime } from './debugRuntime.ts'
|
|
16
|
-
import { NS as SETTINGS_NS, en, zh } from './locales.ts'
|
|
16
|
+
import { NS as SETTINGS_NS, CHAT_NS, en, zh, chatEn, chatZh } from './locales.ts'
|
|
17
17
|
import { DEFAULT_STREAM_CONFIG, STREAM_BOOT_GLOBAL, type StreamConfig } from '../config.ts'
|
|
18
18
|
import { DEFAULT_STREAM_SETTINGS, STREAM_SETTINGS_NS, type StreamSettings } from '../settings.ts'
|
|
19
19
|
|
|
@@ -37,8 +37,34 @@ const STREAM_PRESETS: readonly string[] = ['realtime', 'balanced', 'silky']
|
|
|
37
37
|
* generic follow boundary. This is deliberately keyed by the owner that
|
|
38
38
|
* provides the renderer, not by individual tool names, so new Context,
|
|
39
39
|
* Command, and Tool rows are covered automatically.
|
|
40
|
+
*
|
|
41
|
+
* Host chrome and controllers are excluded on a different rule: `turn-process`
|
|
42
|
+
* (the completed-turn process summary control) and `turn-tail` (the turn usage
|
|
43
|
+
* footer) are not Agent output. The Host renders them as bare controls and
|
|
44
|
+
* flips their visibility itself, so wrapping them has three measured costs and
|
|
45
|
+
* no benefit:
|
|
46
|
+
*
|
|
47
|
+
* 1. The wrapper element defeats ChatView's `.flowItem:empty { display: none }`
|
|
48
|
+
* rule. A `turn-process` renderer returns null whenever the Host's fold is
|
|
49
|
+
* unavailable (e.g. `processWindowReady === false`), and that null render is
|
|
50
|
+
* exactly what the `:empty` rule exists to erase; with a wrapper the row
|
|
51
|
+
* stays in flow and still consumes the 16px column gap, painting an empty
|
|
52
|
+
* band where the fold control should be.
|
|
53
|
+
* 2. The summary label (`N tool calls · M messages`) is not streamed model
|
|
54
|
+
* text, so pushing it through the character-reveal engine is wrong by
|
|
55
|
+
* construction.
|
|
56
|
+
* 3. The row would inherit the generic entrance animation (opacity + clip-path)
|
|
57
|
+
* on a row the Host may mount and hide inside a single commit.
|
|
40
58
|
*/
|
|
41
|
-
const SKIP_WRAP = new Set([
|
|
59
|
+
const SKIP_WRAP = new Set([
|
|
60
|
+
'assistant-step',
|
|
61
|
+
'user',
|
|
62
|
+
'steering',
|
|
63
|
+
'command-input',
|
|
64
|
+
'turn-process',
|
|
65
|
+
'turn-tail',
|
|
66
|
+
'tool-call',
|
|
67
|
+
])
|
|
42
68
|
|
|
43
69
|
/** React function/class or an exotic component such as memo/forwardRef/lazy. */
|
|
44
70
|
function isWrappableComponent(value: unknown): value is ComponentType<FollowWrapProps> {
|
|
@@ -157,6 +183,7 @@ class SettingsCell {
|
|
|
157
183
|
pending === this.pending
|
|
158
184
|
&& next.enabled === this.value.enabled
|
|
159
185
|
&& next.controlScroll === this.value.controlScroll
|
|
186
|
+
&& next.preset === this.value.preset
|
|
160
187
|
&& next.motionPreference === this.value.motionPreference
|
|
161
188
|
&& next.thinkAutoExpand === this.value.thinkAutoExpand
|
|
162
189
|
&& next.logarithmicFade === this.value.logarithmicFade
|
|
@@ -173,6 +200,16 @@ class SettingsCell {
|
|
|
173
200
|
return !this.pending && this.value.enabled
|
|
174
201
|
}
|
|
175
202
|
|
|
203
|
+
/**
|
|
204
|
+
* True only once an attached settings service has produced its loaded values.
|
|
205
|
+
* Until then — and when no settings service is composed at all — `value` is
|
|
206
|
+
* still the shared client default, so callers that must not let that default
|
|
207
|
+
* shadow the install-time config have to ask before trusting it.
|
|
208
|
+
*/
|
|
209
|
+
ready(): boolean {
|
|
210
|
+
return this.card?.getSnapshot().status === 'ready'
|
|
211
|
+
}
|
|
212
|
+
|
|
176
213
|
readonly getSnapshot = (): StreamSettings => this.value
|
|
177
214
|
|
|
178
215
|
readonly subscribe = (listener: () => void): () => void => {
|
|
@@ -201,6 +238,55 @@ export function apply(ctx: ClientContext): void {
|
|
|
201
238
|
() => settings.getSnapshot().controlScroll,
|
|
202
239
|
)
|
|
203
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Layered `t` for the assistant renderer.
|
|
243
|
+
*
|
|
244
|
+
* The renderer's keys have no single owner: `conversation` owns the
|
|
245
|
+
* `image.*` family in every Harness version, `chat` (0.1.5+) owns
|
|
246
|
+
* `message.think`, and three `message.*` keys MOVED from `conversation` to
|
|
247
|
+
* `chat` between the version this package pins and the current one. Binding
|
|
248
|
+
* the slot to either namespace alone degrades a real slice of the UI to raw
|
|
249
|
+
* keys, which is the defect this replaces.
|
|
250
|
+
*
|
|
251
|
+
* The order is deliberate: `conversation` first, so the pinned Harness keeps
|
|
252
|
+
* resolving the keys it still owns there; then `chat` for what moved or is
|
|
253
|
+
* new; then this plugin's own namespace for keys no Harness version
|
|
254
|
+
* provides. A namespace that is not registered returns its key unchanged
|
|
255
|
+
* (`LocaleRuntime.bind` does not throw), so an older Harness without `chat`
|
|
256
|
+
* falls straight through.
|
|
257
|
+
*
|
|
258
|
+
* The reference is built once and held stable: the seat feeds a memoized
|
|
259
|
+
* renderer, and a fresh identity per render would defeat that memoization.
|
|
260
|
+
* Until the locale service arrives the seat's own binding stays in use.
|
|
261
|
+
*/
|
|
262
|
+
let assistantT: AssistantProps['t'] | undefined
|
|
263
|
+
|
|
264
|
+
// Locale routing belongs to the renderer lifecycle, not to the optional
|
|
265
|
+
// Connection-backed settings card. Keeping this injection independent means
|
|
266
|
+
// a deployment can stream localized replies without Connection, and a
|
|
267
|
+
// transient disconnect cannot dispose the renderer's fallback dictionary.
|
|
268
|
+
ctx.inject(['locale'], (localeCtx) => {
|
|
269
|
+
localeCtx.effect(
|
|
270
|
+
() => localeCtx.locale.register(CHAT_NS, { zh: chatZh, en: chatEn }),
|
|
271
|
+
'dsh-smooth-stream: conversation fallback dictionary',
|
|
272
|
+
)
|
|
273
|
+
const conversationT = localeCtx.locale.bind('conversation')
|
|
274
|
+
const chatT = localeCtx.locale.bind('chat') as (key: string, params?: Record<string, unknown>) => string
|
|
275
|
+
const fallbackT = localeCtx.locale.bind(CHAT_NS) as (key: string, params?: Record<string, unknown>) => string
|
|
276
|
+
const merged = (key: string, params?: Record<string, unknown>): string => {
|
|
277
|
+
const primary = (conversationT as (k: string, p?: unknown) => string)(key, params)
|
|
278
|
+
if (primary !== key) return primary
|
|
279
|
+
const secondary = chatT(key, params)
|
|
280
|
+
if (secondary !== key) return secondary
|
|
281
|
+
return fallbackT(key, params)
|
|
282
|
+
}
|
|
283
|
+
const bound = merged as unknown as AssistantProps['t']
|
|
284
|
+
assistantT = bound
|
|
285
|
+
return () => {
|
|
286
|
+
if (assistantT === bound) assistantT = undefined
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
|
|
204
290
|
// The card talks to the plugin-owned loopback RPC, so the core settings
|
|
205
291
|
// namespace allowlist cannot make it disappear. The stream still applies
|
|
206
292
|
// with defaults when the optional Settings UI or Connection is absent.
|
|
@@ -265,8 +351,18 @@ export function apply(ctx: ClientContext): void {
|
|
|
265
351
|
)
|
|
266
352
|
return createElement(TypewriterAssistantNodeView, {
|
|
267
353
|
...props,
|
|
354
|
+
// Override the seat's single-namespace binding with the layered lookup
|
|
355
|
+
// described above. Falls back to the seat's own binding only if the
|
|
356
|
+
// locale service never arrived (renderer then receives the prop it
|
|
357
|
+
// already had).
|
|
358
|
+
...(assistantT === undefined ? {} : { t: assistantT }),
|
|
268
359
|
mode: config.mode,
|
|
269
|
-
preset
|
|
360
|
+
// Loaded settings hold the resolved preset (user layer over the install
|
|
361
|
+
// config), so they own the prop. Without them — no settings service, a
|
|
362
|
+
// failed load, or the frames before the first read lands — the shared
|
|
363
|
+
// client default is NOT an authority on the installed preset; the boot
|
|
364
|
+
// config is.
|
|
365
|
+
preset: settings.ready() ? preferences.preset ?? config.preset : config.preset,
|
|
270
366
|
revealCharsPerSec: config.revealCharsPerSec,
|
|
271
367
|
scrollSpeedPxPerSec: config.scrollSpeedPxPerSec,
|
|
272
368
|
maxScrollSpeedPxPerSec: config.maxScrollSpeedPxPerSec,
|
|
@@ -278,6 +374,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
278
374
|
}
|
|
279
375
|
ctx.slots.inject('conversation.chat.node', () => {
|
|
280
376
|
let releaseTakeover: (() => void) | undefined
|
|
377
|
+
let usedSwap = false
|
|
281
378
|
|
|
282
379
|
const syncTakeover = (): void => {
|
|
283
380
|
if (!settings.takeoverEnabled()) {
|
|
@@ -287,24 +384,67 @@ export function apply(ctx: ClientContext): void {
|
|
|
287
384
|
}
|
|
288
385
|
if (releaseTakeover !== undefined) return
|
|
289
386
|
const unwrap = wrapAgentChatRows(ctx, useControlScroll)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
387
|
+
// dsh 0.2.x renders every keyed entry of `conversation.chat.node` as
|
|
388
|
+
// its own seat, so a lower-priority registration no longer shadows the
|
|
389
|
+
// built-in assistant row — the same assistant step then renders twice
|
|
390
|
+
// (built-in + takeover), duplicating every reasoning block in the
|
|
391
|
+
// transcript. Swap the built-in entry's component in place instead,
|
|
392
|
+
// exactly like the other keyed rows below; this keeps the original
|
|
393
|
+
// children, locale, and inject seats on every kernel generation. The
|
|
394
|
+
// shadow registration survives only as a fallback for compositions
|
|
395
|
+
// that declare no built-in assistant row to swap.
|
|
396
|
+
const builtin = ctx.slots.entries('conversation.chat.node').find(
|
|
397
|
+
entry => entry.options.key === 'assistant-step'
|
|
398
|
+
// The pinned SlotOptions type predates `registrant`; every current
|
|
399
|
+
// runtime carries it (see the register spread in the slots package).
|
|
400
|
+
&& (entry.options as { registrant?: string }).registrant !== 'dsh-smooth-stream'
|
|
401
|
+
&& entry.component !== configured,
|
|
402
|
+
)
|
|
403
|
+
let unshadow: () => void
|
|
404
|
+
if (builtin !== undefined && isWrappableComponent(builtin.component)) {
|
|
405
|
+
usedSwap = true
|
|
406
|
+
const inner = builtin.component as ComponentType<FollowWrapProps>
|
|
407
|
+
builtin.component = configured
|
|
408
|
+
unshadow = () => {
|
|
409
|
+
if (builtin.component === configured) builtin.component = inner
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
unshadow = ctx.slots.register({
|
|
413
|
+
name: 'conversation.chat.node',
|
|
414
|
+
key: 'assistant-step',
|
|
415
|
+
priority: -100,
|
|
416
|
+
// `conversation` (not `chat`): it is the namespace the pinned Harness
|
|
417
|
+
// actually registers, so it is the one whose keys must keep resolving.
|
|
418
|
+
// The layered `t` handed to the renderer covers what this namespace
|
|
419
|
+
// does not own. A namespace the composition does not declare would
|
|
420
|
+
// degrade every `t()` call to its raw key.
|
|
421
|
+
locale: 'conversation',
|
|
422
|
+
registrant: 'dsh-smooth-stream',
|
|
423
|
+
}, configured)
|
|
424
|
+
}
|
|
297
425
|
releaseTakeover = () => {
|
|
298
|
-
// Stop observing slot changes before the
|
|
426
|
+
// Stop observing slot changes before the takeover is removed.
|
|
299
427
|
unwrap()
|
|
300
428
|
unshadow()
|
|
301
429
|
}
|
|
302
430
|
}
|
|
303
431
|
|
|
432
|
+
// If the shadow fallback was taken because the built-in assistant row
|
|
433
|
+
// had not registered yet, re-take through the in-place swap as soon as
|
|
434
|
+
// it appears, so 0.2.x never renders the row twice.
|
|
435
|
+
const offSlotChange = ctx.on('slots/changed', (key: string) => {
|
|
436
|
+
if (key !== 'conversation.chat.node' || usedSwap || releaseTakeover === undefined) return
|
|
437
|
+
if (!settings.takeoverEnabled()) return
|
|
438
|
+
releaseTakeover()
|
|
439
|
+
releaseTakeover = undefined
|
|
440
|
+
syncTakeover()
|
|
441
|
+
})
|
|
442
|
+
|
|
304
443
|
const unsubscribe = settings.subscribe(syncTakeover)
|
|
305
444
|
syncTakeover()
|
|
306
445
|
return () => {
|
|
307
446
|
unsubscribe()
|
|
447
|
+
offSlotChange()
|
|
308
448
|
releaseTakeover?.()
|
|
309
449
|
}
|
|
310
450
|
})
|
package/src/client/locales.ts
CHANGED
|
@@ -3,11 +3,59 @@
|
|
|
3
3
|
/** Dictionary namespace owned by this plugin's settings card. */
|
|
4
4
|
export const NS = 'settings.smoothStream'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Namespace owning the conversation keys THIS PLUGIN must be able to render
|
|
8
|
+
* but the Harness does not always provide.
|
|
9
|
+
*
|
|
10
|
+
* The plugin consumes keys from two Harness namespaces, and which one owns a
|
|
11
|
+
* given key is not stable across Harness versions:
|
|
12
|
+
*
|
|
13
|
+
* - `conversation` (`dsh-client-ui-conversation`) owns the `image.*` family in
|
|
14
|
+
* every version, and also owned `message.stopped`, `message.unknownBlock`
|
|
15
|
+
* and `json.truncated` before those moved to `chat`.
|
|
16
|
+
* - `chat` (`dsh-client-ui-chat`, present from 0.1.5 on) owns `message.think`
|
|
17
|
+
* and now owns the three keys above. It does not exist at all in rc.6, the
|
|
18
|
+
* version this package still pins in devDependencies.
|
|
19
|
+
*
|
|
20
|
+
* Neither namespace alone can serve every key this plugin renders, so
|
|
21
|
+
* `index.ts` layers them. This plugin-owned namespace is the final layer; it
|
|
22
|
+
* exists because a plugin may NOT register into `chat` or `conversation` —
|
|
23
|
+
* `LocaleRuntime.register` throws when the (namespace, locale) pair already
|
|
24
|
+
* has an owner, and the Harness owns both.
|
|
25
|
+
*/
|
|
26
|
+
export const CHAT_NS = 'smoothStream.chat'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Keys the plugin resolves through {@link CHAT_NS}. Only keys with no
|
|
30
|
+
* dependable Harness owner belong here; anything the Harness already provides
|
|
31
|
+
* must keep coming from the Harness so a language pack can override it.
|
|
32
|
+
*/
|
|
33
|
+
export type SmoothStreamChatKey = 'message.think' | 'image.serviceUnavailable'
|
|
34
|
+
|
|
35
|
+
/** English fallback for {@link SmoothStreamChatKey}. */
|
|
36
|
+
export const chatEn: Record<SmoothStreamChatKey, string> = {
|
|
37
|
+
'message.think': 'Think',
|
|
38
|
+
// Removed from the Harness dictionaries in 0.1.5 (rc.6 had it under
|
|
39
|
+
// `conversation`), and the plugin interpolates it into a thrown Error, so
|
|
40
|
+
// the raw key must never reach a reader.
|
|
41
|
+
'image.serviceUnavailable': 'Image service unavailable',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Simplified Chinese fallback for {@link SmoothStreamChatKey}. */
|
|
45
|
+
export const chatZh: Record<SmoothStreamChatKey, string> = {
|
|
46
|
+
'message.think': '思考',
|
|
47
|
+
'image.serviceUnavailable': '图片服务不可用',
|
|
48
|
+
}
|
|
49
|
+
|
|
6
50
|
/** Locale keys the card renders. */
|
|
7
51
|
export type SmoothStreamLocaleKey =
|
|
8
52
|
| 'title' | 'description'
|
|
9
53
|
| 'enabled' | 'enabledHint'
|
|
10
54
|
| 'controlScroll' | 'controlScrollHint'
|
|
55
|
+
| 'preset' | 'presetHint'
|
|
56
|
+
| 'presetRealtime' | 'presetRealtimeHint'
|
|
57
|
+
| 'presetBalanced' | 'presetBalancedHint'
|
|
58
|
+
| 'presetSilky' | 'presetSilkyHint'
|
|
11
59
|
| 'motionPreference' | 'motionPreferenceHint'
|
|
12
60
|
| 'motionAuto' | 'motionAutoHint'
|
|
13
61
|
| 'motionForceSmooth' | 'motionForceSmoothHint'
|
|
@@ -22,6 +70,8 @@ export type SmoothStreamLocaleKey =
|
|
|
22
70
|
| 'debugFps' | 'debugFrameTime' | 'debugBacklog' | 'debugRevealSpeed' | 'debugProgress'
|
|
23
71
|
| 'debugFollowState' | 'debugFollowing' | 'debugReleased'
|
|
24
72
|
| 'debugLag' | 'debugVelocity' | 'debugReserve' | 'debugCapacity' | 'debugAppliedScale'
|
|
73
|
+
| 'debugTerminalPhase' | 'debugOwnedRunway' | 'debugTerminalBudget' | 'debugBaselineShift'
|
|
74
|
+
| 'debugAnchorDelta' | 'debugPhaseLive' | 'debugPhaseDrain' | 'debugPhaseCascade' | 'debugPhaseNatural'
|
|
25
75
|
| 'debugRevealMultiplier' | 'debugQueuePressure' | 'debugMaxReveal'
|
|
26
76
|
| 'debugSpringStiffness' | 'debugSpringDamping' | 'debugSpringMass'
|
|
27
77
|
| 'debugRunway' | 'debugReserveResponse' | 'debugBackpressureMin'
|
|
@@ -42,6 +92,14 @@ export const en: Record<SmoothStreamLocaleKey, string> = {
|
|
|
42
92
|
enabledHint: 'Let this plugin render and follow streaming replies. Turn off to use the built-in Harness renderer.',
|
|
43
93
|
controlScroll: 'Take over scrolling',
|
|
44
94
|
controlScrollHint: 'On by default; smooth-stream writes the conversation scroll position. Turn off to leave bottom-follow to Harness.',
|
|
95
|
+
preset: 'Pacing preset',
|
|
96
|
+
presetHint: 'Smoothing cadence profile for text reveal and glide.',
|
|
97
|
+
presetRealtime: 'Realtime',
|
|
98
|
+
presetRealtimeHint: 'Aggressive cadence, low latency, tracks output closely.',
|
|
99
|
+
presetBalanced: 'Balanced',
|
|
100
|
+
presetBalancedHint: 'Default balance between pace and visual rhythm.',
|
|
101
|
+
presetSilky: 'Silky',
|
|
102
|
+
presetSilkyHint: 'Higher backlog buffer, gentle EMA damping, maximum fluidity.',
|
|
45
103
|
motionPreference: 'Motion',
|
|
46
104
|
motionPreferenceHint: 'How streaming responds to the system reduce-motion setting.',
|
|
47
105
|
motionAuto: 'Follow system',
|
|
@@ -85,6 +143,15 @@ export const en: Record<SmoothStreamLocaleKey, string> = {
|
|
|
85
143
|
debugReserve: 'Reserve',
|
|
86
144
|
debugCapacity: 'Capacity',
|
|
87
145
|
debugAppliedScale: 'Applied scale',
|
|
146
|
+
debugTerminalPhase: 'Terminal phase',
|
|
147
|
+
debugOwnedRunway: 'Owned runway',
|
|
148
|
+
debugTerminalBudget: 'Terminal budget',
|
|
149
|
+
debugBaselineShift: 'Baseline shift',
|
|
150
|
+
debugAnchorDelta: 'Anchor Δ',
|
|
151
|
+
debugPhaseLive: 'live',
|
|
152
|
+
debugPhaseDrain: 'terminal-drain',
|
|
153
|
+
debugPhaseCascade: 'host-cascade',
|
|
154
|
+
debugPhaseNatural: 'natural',
|
|
88
155
|
debugRevealMultiplier: 'Reveal multiplier',
|
|
89
156
|
debugQueuePressure: 'Queue pressure',
|
|
90
157
|
debugMaxReveal: 'Maximum reveal',
|
|
@@ -132,6 +199,14 @@ export const zh: Record<SmoothStreamLocaleKey, string> = {
|
|
|
132
199
|
enabledHint: '由本插件渲染并跟随流式回复;关闭后使用 Harness 内置渲染。',
|
|
133
200
|
controlScroll: '接管滚动',
|
|
134
201
|
controlScrollHint: '默认开启,由丝滑流式写入会话滚动位置;关闭后贴底滚动交给 Harness。',
|
|
202
|
+
preset: '手感预设',
|
|
203
|
+
presetHint: '文字吐字节奏与视口跟随阻尼风格。',
|
|
204
|
+
presetRealtime: '激进快速 (realtime)',
|
|
205
|
+
presetRealtimeHint: '缓冲低、流速快,紧跟模型生成。',
|
|
206
|
+
presetBalanced: '均衡自然 (balanced)',
|
|
207
|
+
presetBalancedHint: '官方默认,平衡流速与节奏感。',
|
|
208
|
+
presetSilky: '极致丝滑 (silky)',
|
|
209
|
+
presetSilkyHint: '字符积压缓冲加大、阻尼更高、EMA 平滑度更好,视觉呼吸感最强。',
|
|
135
210
|
motionPreference: '动效偏好',
|
|
136
211
|
motionPreferenceHint: '流式渲染如何响应系统的「减少动态效果」设置。',
|
|
137
212
|
motionAuto: '跟随系统',
|
|
@@ -175,6 +250,15 @@ export const zh: Record<SmoothStreamLocaleKey, string> = {
|
|
|
175
250
|
debugReserve: '预留空间',
|
|
176
251
|
debugCapacity: '安全容量',
|
|
177
252
|
debugAppliedScale: '实际倍率',
|
|
253
|
+
debugTerminalPhase: '收尾阶段',
|
|
254
|
+
debugOwnedRunway: '实体留白',
|
|
255
|
+
debugTerminalBudget: '收尾预算',
|
|
256
|
+
debugBaselineShift: '基线位移',
|
|
257
|
+
debugAnchorDelta: '锚点位移',
|
|
258
|
+
debugPhaseLive: '流式中',
|
|
259
|
+
debugPhaseDrain: '收尾排空',
|
|
260
|
+
debugPhaseCascade: '宿主交接',
|
|
261
|
+
debugPhaseNatural: '自然态',
|
|
178
262
|
debugRevealMultiplier: '揭示倍率',
|
|
179
263
|
debugQueuePressure: '队列压力',
|
|
180
264
|
debugMaxReveal: '最大揭示速度',
|
|
@@ -217,5 +301,6 @@ export const zh: Record<SmoothStreamLocaleKey, string> = {
|
|
|
217
301
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
218
302
|
interface LocaleNamespaceMap {
|
|
219
303
|
'settings.smoothStream': SmoothStreamLocaleKey
|
|
304
|
+
'smoothStream.chat': SmoothStreamChatKey
|
|
220
305
|
}
|
|
221
306
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type StreamDebugTuning,
|
|
8
8
|
type StreamMotionPreference,
|
|
9
9
|
type StreamSettings,
|
|
10
|
+
type StreamSmoothingPreset,
|
|
10
11
|
} from '../settings.ts'
|
|
11
12
|
import type {
|
|
12
13
|
StreamDebugSettingsView,
|
|
@@ -24,6 +25,7 @@ export interface SmoothStreamCardState {
|
|
|
24
25
|
failed: boolean
|
|
25
26
|
enabled: boolean
|
|
26
27
|
controlScroll: boolean
|
|
28
|
+
preset: StreamSmoothingPreset
|
|
27
29
|
motionPreference: StreamMotionPreference
|
|
28
30
|
thinkAutoExpand: boolean
|
|
29
31
|
logarithmicFade: boolean
|
|
@@ -55,7 +57,7 @@ export class SmoothStreamCardController {
|
|
|
55
57
|
private readonly store = createSnapshotStore<SmoothStreamCardState>(this.projection())
|
|
56
58
|
private loaded: StreamSettingsView | undefined
|
|
57
59
|
private loadedDebug: StreamDebugSettingsView | undefined
|
|
58
|
-
private stagedBase: Pick<StreamSettings, 'enabled' | 'controlScroll' | 'motionPreference' | 'thinkAutoExpand' | 'logarithmicFade'> | undefined
|
|
60
|
+
private stagedBase: Pick<StreamSettings, 'enabled' | 'controlScroll' | 'preset' | 'motionPreference' | 'thinkAutoExpand' | 'logarithmicFade'> | undefined
|
|
59
61
|
private stagedDebug: Pick<StreamSettings, 'debugEnabled' | 'debugTuning'> | undefined
|
|
60
62
|
private saving = false
|
|
61
63
|
private failed = false
|
|
@@ -93,11 +95,12 @@ export class SmoothStreamCardController {
|
|
|
93
95
|
hooks: { smoothStreamCard: this.store },
|
|
94
96
|
edit: (patch) => {
|
|
95
97
|
if (this.saving) return
|
|
96
|
-
if (patch.enabled !== undefined || patch.controlScroll !== undefined || patch.motionPreference !== undefined || patch.thinkAutoExpand !== undefined || patch.logarithmicFade !== undefined) {
|
|
98
|
+
if (patch.enabled !== undefined || patch.controlScroll !== undefined || patch.preset !== undefined || patch.motionPreference !== undefined || patch.thinkAutoExpand !== undefined || patch.logarithmicFade !== undefined) {
|
|
97
99
|
this.stagedBase = {
|
|
98
100
|
...this.baseValues(),
|
|
99
101
|
...(patch.enabled === undefined ? {} : { enabled: patch.enabled }),
|
|
100
102
|
...(patch.controlScroll === undefined ? {} : { controlScroll: patch.controlScroll }),
|
|
103
|
+
...(patch.preset === undefined ? {} : { preset: patch.preset }),
|
|
101
104
|
...(patch.motionPreference === undefined ? {} : { motionPreference: patch.motionPreference }),
|
|
102
105
|
...(patch.thinkAutoExpand === undefined ? {} : { thinkAutoExpand: patch.thinkAutoExpand }),
|
|
103
106
|
...(patch.logarithmicFade === undefined ? {} : { logarithmicFade: patch.logarithmicFade }),
|
|
@@ -145,10 +148,11 @@ export class SmoothStreamCardController {
|
|
|
145
148
|
}
|
|
146
149
|
}
|
|
147
150
|
|
|
148
|
-
private baseValues(): Pick<StreamSettings, 'enabled' | 'controlScroll' | 'motionPreference' | 'thinkAutoExpand' | 'logarithmicFade'> {
|
|
151
|
+
private baseValues(): Pick<StreamSettings, 'enabled' | 'controlScroll' | 'preset' | 'motionPreference' | 'thinkAutoExpand' | 'logarithmicFade'> {
|
|
149
152
|
return this.stagedBase ?? {
|
|
150
153
|
enabled: this.loaded?.enabled ?? DEFAULT_STREAM_SETTINGS.enabled,
|
|
151
154
|
controlScroll: this.loaded?.controlScroll ?? DEFAULT_STREAM_SETTINGS.controlScroll,
|
|
155
|
+
preset: this.loaded?.preset ?? DEFAULT_STREAM_SETTINGS.preset,
|
|
152
156
|
motionPreference: this.loaded?.motionPreference ?? DEFAULT_STREAM_SETTINGS.motionPreference,
|
|
153
157
|
thinkAutoExpand: this.loaded?.thinkAutoExpand ?? DEFAULT_STREAM_SETTINGS.thinkAutoExpand,
|
|
154
158
|
logarithmicFade: this.loaded?.logarithmicFade ?? DEFAULT_STREAM_SETTINGS.logarithmicFade,
|
|
@@ -24,13 +24,14 @@ function settingsView(value: unknown): StreamSettingsView {
|
|
|
24
24
|
|| typeof data.writable !== 'boolean'
|
|
25
25
|
|| typeof data.enabled !== 'boolean'
|
|
26
26
|
|| typeof data.controlScroll !== 'boolean'
|
|
27
|
+
|| (data.preset !== undefined && !['realtime', 'balanced', 'silky'].includes(data.preset as string))
|
|
27
28
|
|| !['auto', 'force-smooth', 'force-reduced'].includes(data.motionPreference as string)
|
|
28
29
|
|| typeof data.thinkAutoExpand !== 'boolean'
|
|
29
30
|
|| (data.logarithmicFade !== undefined && typeof data.logarithmicFade !== 'boolean')
|
|
30
31
|
|| typeof data.canUpgrade !== 'boolean') {
|
|
31
32
|
throw new Error('dsh-smooth-stream: malformed settings response')
|
|
32
33
|
}
|
|
33
|
-
return { ...data, logarithmicFade: data.logarithmicFade ?? true } as unknown as StreamSettingsView
|
|
34
|
+
return { ...data, preset: (data.preset ?? 'balanced') as StreamSettingsView['preset'], logarithmicFade: data.logarithmicFade ?? true } as unknown as StreamSettingsView
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
function upgradeView(value: unknown): StreamUpgradeView {
|
|
@@ -82,13 +83,14 @@ export function createSmoothStreamSettingsApi(connection: ConnectionHandle): Smo
|
|
|
82
83
|
async read(): Promise<StreamSettingsView> {
|
|
83
84
|
return settingsView(accepted(await connection.rpc.call(STREAM_SETTINGS_RPC_CHANNEL, STREAM_SETTINGS_RPC.read, {})))
|
|
84
85
|
},
|
|
85
|
-
async write(settings: Pick<StreamSettings, 'enabled' | 'controlScroll' | 'motionPreference' | 'thinkAutoExpand' | 'logarithmicFade'> & Partial<Pick<StreamSettings, 'debugEnabled' | 'debugTuning'>>): Promise<StreamSettingsView> {
|
|
86
|
+
async write(settings: Pick<StreamSettings, 'enabled' | 'controlScroll' | 'preset' | 'motionPreference' | 'thinkAutoExpand' | 'logarithmicFade'> & Partial<Pick<StreamSettings, 'debugEnabled' | 'debugTuning'>>): Promise<StreamSettingsView> {
|
|
86
87
|
return settingsView(accepted(await connection.rpc.call(
|
|
87
88
|
STREAM_SETTINGS_RPC_CHANNEL,
|
|
88
89
|
STREAM_SETTINGS_RPC.write,
|
|
89
90
|
{
|
|
90
91
|
enabled: settings.enabled,
|
|
91
92
|
controlScroll: settings.controlScroll,
|
|
93
|
+
preset: settings.preset,
|
|
92
94
|
motionPreference: settings.motionPreference,
|
|
93
95
|
thinkAutoExpand: settings.thinkAutoExpand,
|
|
94
96
|
logarithmicFade: settings.logarithmicFade,
|