From 0c13a1723183b7ded96f9acab6c001fcbd293313 Mon Sep 17 00:00:00 2001 From: ruizanthony Date: Wed, 1 Jul 2026 07:38:44 +0000 Subject: [PATCH 1/2] fix(webui): stop live streaming message flicker Opt live assistant streaming nodes out of the global theme color/background transitions so token-by-token markdown updates do not flash or fade on light themes. Adds a targeted regression test in test_smooth_text_fade.py while keeping the opt-in smooth text fade feature intact. --- static/style.css | 15 +++++++++++++++ tests/test_smooth_text_fade.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/static/style.css b/static/style.css index c9cf00b39..721540a25 100644 --- a/static/style.css +++ b/static/style.css @@ -1458,6 +1458,21 @@ :root:not(.dark) .cron-btn:hover{background:rgba(0,0,0,.08);} /* ── Smooth dark mode transitions ── */ body,header,footer,aside,nav,main,div,button,input,textarea,select{transition-property:background-color,border-color,color;transition-duration:.15s;transition-timing-function:ease;} + /* Live assistant content is updated token-by-token. The global div color/background + transition above makes each streamed markdown rewrite briefly fade/flash on light + themes. Keep theme transitions elsewhere, but make the live turn paint instantly. */ + #liveAssistantTurn, + #liveAssistantTurn *, + #thinkingRow, + #thinkingRow *, + .assistant-segment[data-live-assistant="1"], + .assistant-segment[data-live-assistant="1"] *, + .agent-activity-thinking[data-thinking-active="1"], + .agent-activity-thinking[data-thinking-active="1"] *, + .agent-activity-thinking[data-live-thinking="1"], + .agent-activity-thinking[data-live-thinking="1"] *, + .live-worklog[data-live-worklog-shell="1"], + .live-worklog[data-live-worklog-shell="1"] *{transition-property:none!important;transition-duration:0s!important;transition-delay:0s!important;} :root{--app-titlebar-safe-top:0px;} .pwa-standalone{overscroll-behavior:none;} .pwa-standalone body{-webkit-tap-highlight-color:transparent;} diff --git a/tests/test_smooth_text_fade.py b/tests/test_smooth_text_fade.py index aaf918ff5..c132b3afc 100644 --- a/tests/test_smooth_text_fade.py +++ b/tests/test_smooth_text_fade.py @@ -197,6 +197,35 @@ def test_stream_fade_done_drain_has_hard_cap_for_large_buffered_responses(): ) +def test_live_streaming_assistant_content_opts_out_of_global_theme_transitions(): + """Per-token markdown rewrites must not inherit global div color/background fades. + + The global theme transition is useful for dark/light switches, but live + assistant DOM updates happen for every streamed token. If those live nodes + inherit color/background transitions, light themes visibly flash/fade on + each word. + """ + live_transition_guard = STYLE_CSS[ + STYLE_CSS.index("Live assistant content is updated token-by-token") : STYLE_CSS.index( + ":root{--app-titlebar-safe-top" + ) + ] + assert_contains_all( + live_transition_guard, + [ + "#liveAssistantTurn *", + "#thinkingRow *", + '.assistant-segment[data-live-assistant="1"] *', + '.agent-activity-thinking[data-thinking-active="1"] *', + '.agent-activity-thinking[data-live-thinking="1"] *', + '.live-worklog[data-live-worklog-shell="1"] *', + "transition-property:none!important", + "transition-duration:0s!important", + "transition-delay:0s!important", + ], + ) + + 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( From 76ae2b795cbc13900eac381d47db91ff7fabc077 Mon Sep 17 00:00:00 2001 From: Anthony Ruiz Date: Wed, 1 Jul 2026 15:50:24 +0000 Subject: [PATCH 2/2] test(webui): make live-transition guard anchors explicit --- tests/test_smooth_text_fade.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/test_smooth_text_fade.py b/tests/test_smooth_text_fade.py index c132b3afc..5a1cd78a9 100644 --- a/tests/test_smooth_text_fade.py +++ b/tests/test_smooth_text_fade.py @@ -55,6 +55,14 @@ def assert_contains_all(src: str, snippets: list[str]) -> None: assert snippet in src +def slice_between(src: str, start_anchor: str, end_anchor: str) -> str: + start = src.find(start_anchor) + assert start != -1, f"start anchor not found: {start_anchor!r}" + end = src.find(end_anchor, start + len(start_anchor)) + assert end != -1, f"end anchor not found after {start_anchor!r}: {end_anchor!r}" + return src[start:end] + + def fade_helper_script(performance_stub: str = "{_t:0,now(){return this._t;}}") -> str: helpers = "\n".join( function_block(MESSAGES_JS, name) @@ -205,11 +213,11 @@ def test_live_streaming_assistant_content_opts_out_of_global_theme_transitions() inherit color/background transitions, light themes visibly flash/fade on each word. """ - live_transition_guard = STYLE_CSS[ - STYLE_CSS.index("Live assistant content is updated token-by-token") : STYLE_CSS.index( - ":root{--app-titlebar-safe-top" - ) - ] + live_transition_guard = slice_between( + STYLE_CSS, + "Live assistant content is updated token-by-token", + ":root{--app-titlebar-safe-top", + ) assert_contains_all( live_transition_guard, [