dsh-smooth-stream 0.4.0 → 0.4.2
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 +41 -7
- package/README.md +40 -6
- package/lib/client.js +2923 -2392
- package/lib/client.js.map +1 -1
- package/lib/index.js +1 -1
- package/package.json +17 -13
- package/src/client/AnimatedDisclosure.tsx +14 -1
- package/src/client/FollowHost.tsx +21 -4
- package/src/client/TypewriterAssistantNodeView.module.css +28 -1
- package/src/client/TypewriterAssistantNodeView.tsx +36 -3
- package/src/client/auto-collapse-controller.ts +137 -42
- package/src/client/clientStore.ts +41 -0
- package/src/client/debugRuntime.ts +1 -1
- package/src/client/smooth-stream-card-controller.ts +1 -1
- package/src/client/teleprompterGlide.ts +900 -117
- package/src/client/useSmoothStreamContent.ts +71 -15
- package/src/settings.ts +1 -1
|
@@ -170,14 +170,24 @@ export function computeSettleDrain(config: StreamSmoothingPresetConfig, input: S
|
|
|
170
170
|
return clamp(Math.max(settleCps, overflowCps), config.flushCps, config.maxFlushCps)
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
/**
|
|
173
|
+
/**
|
|
174
|
+
* Fixed velocity that closes a producer-complete queue within its deadline.
|
|
175
|
+
* This completion-only target may exceed the live-stream flush ceiling.
|
|
176
|
+
*/
|
|
174
177
|
export function computeCompletionDrain(
|
|
175
178
|
config: StreamSmoothingPresetConfig,
|
|
176
179
|
backlog: number,
|
|
180
|
+
initialCps = 0,
|
|
177
181
|
): number {
|
|
178
182
|
if (backlog <= 0) return 0
|
|
179
183
|
const drainTargetMs = clamp(backlog * 8, config.settleDrainMinMs, config.settleDrainMaxMs)
|
|
180
|
-
const
|
|
184
|
+
const deadlineSeconds = drainTargetMs / 1000
|
|
185
|
+
const rampAreaSeconds = SETTLE_RAMP_TAU_S * (1 - Math.exp(
|
|
186
|
+
-deadlineSeconds / SETTLE_RAMP_TAU_S,
|
|
187
|
+
))
|
|
188
|
+
const deadlineCps = (
|
|
189
|
+
backlog - Math.max(0, initialCps) * rampAreaSeconds
|
|
190
|
+
) / Math.max(0.001, deadlineSeconds - rampAreaSeconds)
|
|
181
191
|
return Math.max(
|
|
182
192
|
deadlineCps,
|
|
183
193
|
computeSettleDrain(config, { backlog, inputActive: false, settling: true }),
|
|
@@ -189,6 +199,8 @@ export function computeCompletionDrain(
|
|
|
189
199
|
* this multiple of the steady rate, so the end never drags.
|
|
190
200
|
*/
|
|
191
201
|
export const SETTLE_DRAIN_MULTIPLIER = 1.8
|
|
202
|
+
/** Time constant for ramping the completion-drain velocity up from streaming pace. */
|
|
203
|
+
export const SETTLE_RAMP_TAU_S = 0.09
|
|
192
204
|
|
|
193
205
|
export interface RevealStepInput {
|
|
194
206
|
readonly backlog: number
|
|
@@ -261,6 +273,14 @@ export interface UseSmoothStreamContentOptions {
|
|
|
261
273
|
defaultCps?: number | undefined
|
|
262
274
|
/** Written each commit with the live arrival-rate EMA for the follow lerp. */
|
|
263
275
|
speedCpsRef?: { current: number } | undefined
|
|
276
|
+
/** Cumulative code points that have actually committed to the display. */
|
|
277
|
+
revealedCharsRef?: { current: number } | undefined
|
|
278
|
+
/**
|
|
279
|
+
* Called synchronously after each reveal commit lands in the DOM, so the
|
|
280
|
+
* conversation follower can re-run its geometry inside the same task and
|
|
281
|
+
* no intermediate (grown-but-uncompensated) frame can reach a paint.
|
|
282
|
+
*/
|
|
283
|
+
onRevealCommit?: (() => void) | undefined
|
|
264
284
|
/** Live multiplier from the follow spring when safe visual lag is filling. */
|
|
265
285
|
revealScaleRef?: { current: number } | undefined
|
|
266
286
|
}
|
|
@@ -282,7 +302,9 @@ export function useSmoothStreamContent(
|
|
|
282
302
|
steadyCps,
|
|
283
303
|
defaultCps,
|
|
284
304
|
speedCpsRef,
|
|
305
|
+
revealedCharsRef,
|
|
285
306
|
revealScaleRef,
|
|
307
|
+
onRevealCommit,
|
|
286
308
|
}: UseSmoothStreamContentOptions = {},
|
|
287
309
|
): string {
|
|
288
310
|
const config = PRESET_CONFIG[preset]
|
|
@@ -309,11 +331,16 @@ export function useSmoothStreamContent(
|
|
|
309
331
|
const lastFrameTsRef = useRef<number | null>(null)
|
|
310
332
|
const queueDebtRef = useRef(0)
|
|
311
333
|
const settleCpsRef = useRef<number | null>(null)
|
|
334
|
+
const lastDrainCpsRef = useRef(0)
|
|
312
335
|
const holdBackRef = useRef(shouldHoldBack)
|
|
313
336
|
const speedOutRef = useRef(speedCpsRef)
|
|
314
337
|
speedOutRef.current = speedCpsRef
|
|
338
|
+
const revealedCharsOutRef = useRef(revealedCharsRef)
|
|
339
|
+
revealedCharsOutRef.current = revealedCharsRef
|
|
315
340
|
const revealScaleOutRef = useRef(revealScaleRef)
|
|
316
341
|
revealScaleOutRef.current = revealScaleRef
|
|
342
|
+
const onRevealCommitOutRef = useRef(onRevealCommit)
|
|
343
|
+
onRevealCommitOutRef.current = onRevealCommit
|
|
317
344
|
const inputCompleteRef = useRef(inputComplete)
|
|
318
345
|
inputCompleteRef.current = inputComplete
|
|
319
346
|
const streamIdRef = useRef(`stream-${Math.random().toString(36).slice(2)}`)
|
|
@@ -344,6 +371,7 @@ export function useSmoothStreamContent(
|
|
|
344
371
|
displayedCountRef.current = chars.length
|
|
345
372
|
queueDebtRef.current = 0
|
|
346
373
|
settleCpsRef.current = null
|
|
374
|
+
lastDrainCpsRef.current = 0
|
|
347
375
|
const speedOut = speedOutRef.current
|
|
348
376
|
if (speedOut !== undefined) speedOut.current = seedCps
|
|
349
377
|
setDisplayedContent(nextContent)
|
|
@@ -356,14 +384,16 @@ export function useSmoothStreamContent(
|
|
|
356
384
|
[seedCps, stopFrameLoop],
|
|
357
385
|
)
|
|
358
386
|
|
|
359
|
-
// Producer completion
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
387
|
+
// Producer completion used to publish the accumulated source in one commit
|
|
388
|
+
// here. That single commit is a visible teleport whenever a fast stream
|
|
389
|
+
// ends with standing backlog — hundreds of chars become thousands of px in
|
|
390
|
+
// one frame, exactly the end-of-render jerk this plugin fights. The frame
|
|
391
|
+
// loop's producer-complete branch already drains the queue at a bounded
|
|
392
|
+
// constant velocity (computeCompletionDrain), which finishes near its
|
|
393
|
+
// configured deadline and glides every px through the spring. Nothing is left
|
|
394
|
+
// "typing" after the Agent stops; the tail just closes at drain speed.
|
|
395
|
+
//
|
|
396
|
+
// Non-append-only edits and disabled streams still sync immediately below.
|
|
367
397
|
|
|
368
398
|
const startFrameLoop = useCallback(() => {
|
|
369
399
|
if (rafRef.current !== null) return
|
|
@@ -406,14 +436,31 @@ export function useSmoothStreamContent(
|
|
|
406
436
|
if (producerComplete) {
|
|
407
437
|
// Backpressure protects live layout. Once input has ended, retaining
|
|
408
438
|
// that scale only makes a completed response keep typing onscreen.
|
|
409
|
-
// Keep one
|
|
410
|
-
// it from the shrinking backlog creates an exponential slow tail
|
|
411
|
-
|
|
412
|
-
|
|
439
|
+
// Keep one TARGET velocity for the whole completion tail — recomputing
|
|
440
|
+
// it from the shrinking backlog creates an exponential slow tail —
|
|
441
|
+
// but RAMP the effective velocity up from the last reveal speed over
|
|
442
|
+
// ~3 frames: an instant jump from streaming pace to max flush is
|
|
443
|
+
// itself a visible jerk at exactly the moment the reader is watching
|
|
444
|
+
// the reply finish.
|
|
445
|
+
// The pre-drain reveal speed is the streaming EMA, not the debug seed.
|
|
446
|
+
// Seeding the ramp from the 35cps default when the reply ran at the
|
|
447
|
+
// max-flush ceiling stretched a 420ms tail past the 800ms announcer
|
|
448
|
+
// step and the visible Markdown mutated across it.
|
|
449
|
+
const previousCps = lastDrainCpsRef.current > 0
|
|
450
|
+
? lastDrainCpsRef.current
|
|
451
|
+
: Math.max(config.minCps, emaCpsRef.current)
|
|
452
|
+
const drainTargetCps = settleCpsRef.current
|
|
453
|
+
?? computeCompletionDrain(config, backlog, previousCps)
|
|
454
|
+
settleCpsRef.current = drainTargetCps
|
|
455
|
+
const rampedCps = previousCps
|
|
456
|
+
+ (drainTargetCps - previousCps) * (1 - Math.exp(-dtSeconds / SETTLE_RAMP_TAU_S))
|
|
457
|
+
const settleCps = Math.min(drainTargetCps, Math.max(previousCps, rampedCps))
|
|
458
|
+
lastDrainCpsRef.current = Math.min(drainTargetCps, rampedCps)
|
|
413
459
|
const accumulated = Math.max(0, queueDebtRef.current) + settleCps * dtSeconds
|
|
414
460
|
revealChars = Math.min(backlog, Math.floor(accumulated))
|
|
415
461
|
revealSpeedCps = settleCps
|
|
416
462
|
nextQueueDebt = revealChars >= backlog ? 0 : accumulated - revealChars
|
|
463
|
+
if (revealChars >= backlog) lastDrainCpsRef.current = 0
|
|
417
464
|
} else if (steadyCps !== undefined) {
|
|
418
465
|
const step = computeRevealStep(
|
|
419
466
|
config,
|
|
@@ -486,7 +533,16 @@ export function useSmoothStreamContent(
|
|
|
486
533
|
}
|
|
487
534
|
|
|
488
535
|
rafRef.current = requestAnimationFrame(tick)
|
|
489
|
-
}, [config, stopFrameLoop, steadyCps])
|
|
536
|
+
}, [config, seedCps, stopFrameLoop, steadyCps])
|
|
537
|
+
|
|
538
|
+
// Run AFTER the commit's DOM mutations, before paint: the follower's
|
|
539
|
+
// same-task correction hook.
|
|
540
|
+
useLayoutEffect(() => {
|
|
541
|
+
const revealedCharsOut = revealedCharsOutRef.current
|
|
542
|
+
if (revealedCharsOut !== undefined) revealedCharsOut.current = displayedCountRef.current
|
|
543
|
+
if (displayedContent === '') return
|
|
544
|
+
onRevealCommitOutRef.current?.()
|
|
545
|
+
}, [displayedContent])
|
|
490
546
|
|
|
491
547
|
useEffect(() => {
|
|
492
548
|
startFrameLoopRef.current = startFrameLoop
|