fix: cap stream fade done drain

This commit is contained in:
Michael Lam
2026-05-17 00:27:17 -07:00
parent 603183a301
commit 87e3b4e88e
3 changed files with 31 additions and 0 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- **PR #2450** by @Michaelyklam (fixes #2447) — Cap the optional streaming word-fade drain after the final `done` SSE event so very large or bursty completed responses are rendered from the canonical session promptly instead of keeping the chat in a live/working state until Stop is pressed.
## [v0.51.82] — 2026-05-17 — Release BF (stage-375 — 2-PR batch — table renderer pipe protection + Catppuccin appearance skin)
### Added
+12
View File
@@ -697,6 +697,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
const _STREAM_FADE_MAX_MS=350;
const _STREAM_FADE_STAGGER_MS=16;
const _STREAM_FADE_DONE_MAX_MS=320;
const _STREAM_FADE_DONE_DRAIN_MAX_MS=900;
const _streamFadeEnabledForStream=window._fadeTextEffect===true;
// rAF-throttled rendering: buffer tokens, render at most once per frame
@@ -1086,6 +1087,8 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
: _stripXmlToolCalls(assistantText.slice(segmentStart));
}
function _drainStreamFadeBeforeDone(onDone){
const drainStartedAt=performance.now();
let forcedDone=false;
const step=()=>{
if(!assistantBody){onDone();return;}
const target=_streamFadeCurrentDisplayText();
@@ -1101,6 +1104,15 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
setTimeout(onDone, Math.min(remainingAnimationMs, _STREAM_FADE_DONE_MAX_MS));
return;
}
// Final SSE `done` means the canonical completed session is available.
// The optional word-fade playout must not keep that completed answer
// hidden behind the live Thinking state for large/bursty responses.
if(!forcedDone&&performance.now()-drainStartedAt>=_STREAM_FADE_DONE_DRAIN_MAX_MS){
forcedDone=true;
if(_smdParser) _smdEndParser();
onDone();
return;
}
setTimeout(()=>requestAnimationFrame(step), 33);
};
step();
+15
View File
@@ -82,6 +82,7 @@ const _STREAM_FADE_MS=200;
const _STREAM_FADE_MAX_MS=350;
const _STREAM_FADE_STAGGER_MS=16;
const _STREAM_FADE_DONE_MAX_MS=320;
const _STREAM_FADE_DONE_DRAIN_MAX_MS=900;
const performance={performance_stub};
{helpers}
"""
@@ -178,6 +179,20 @@ def test_stream_fade_uses_incremental_renderer_without_changing_default_path():
assert "_wrapStreamingFadeWords" not in MESSAGES_JS
def test_stream_fade_done_drain_has_hard_cap_for_large_buffered_responses():
drain_block = function_block(MESSAGES_JS, "_drainStreamFadeBeforeDone")
assert "const _STREAM_FADE_DONE_DRAIN_MAX_MS=900" in MESSAGES_JS
assert_contains_all(
drain_block,
[
"const drainStartedAt=performance.now();",
"performance.now()-drainStartedAt>=_STREAM_FADE_DONE_DRAIN_MAX_MS",
"if(_smdParser) _smdEndParser();",
"onDone();",
],
)
def test_stream_fade_css_is_opacity_only_and_hides_live_cursor():
fade_css = STYLE_CSS[STYLE_CSS.index("OpenWebUI-style streaming word fade") :]
assert "filter:" not in STYLE_CSS[STYLE_CSS.index("OpenWebUI-style streaming word fade") :].split(