From dd29d99b85e0be0ce3972364be0693f7497d6710 Mon Sep 17 00:00:00 2001 From: Hardik-369 Date: Fri, 3 Jul 2026 00:59:57 +0530 Subject: [PATCH] fix(tui): reduce scroll stutter by throttling UI refresh and caching renders - Increased UI update interval from 350ms to 500ms - Reduced dot animation frequency from 60ms to 250ms - Reduced splash animation frequency from 50ms to 100ms - Added content hash cache for rendered agent messages to avoid re-parsing markdown and re-running Pygments on every tick - Added guard to prevent redundant scroll_end callbacks from queuing during rapid updates Closes #581 --- strix/interface/tui/app.py | 19 ++++++++++++++----- .../tui/renderers/agent_message_renderer.py | 14 ++++++++++++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/strix/interface/tui/app.py b/strix/interface/tui/app.py index 4e43d979..34965168 100644 --- a/strix/interface/tui/app.py +++ b/strix/interface/tui/app.py @@ -128,7 +128,7 @@ class SplashScreen(Static): # type: ignore[misc] yield panel_static def on_mount(self) -> None: - self._animation_timer = self.set_interval(0.05, self._animate_start_line) + self._animation_timer = self.set_interval(0.1, self._animate_start_line) def on_unmount(self) -> None: if self._animation_timer is not None: @@ -729,6 +729,7 @@ class StrixTUIApp(App): # type: ignore[misc] "#86efac", # Brightest ] self._dot_animation_timer: Any | None = None + self._pending_scroll_end = False self._setup_cleanup_handlers() @@ -872,7 +873,7 @@ class StrixTUIApp(App): # type: ignore[misc] self._start_scan_thread() - self.set_interval(0.35, self._update_ui) + self.set_interval(0.5, self._update_ui) def _update_ui(self) -> None: if self.show_splash: @@ -1018,8 +1019,16 @@ class StrixTUIApp(App): # type: ignore[misc] self._safe_widget_operation(chat_display.update, content) chat_display.set_classes(css_class) - if is_at_bottom: - self.call_later(chat_history.scroll_end, animate=False) + if is_at_bottom and not self._pending_scroll_end: + self._pending_scroll_end = True + self.call_later(self._do_scroll_end, chat_history) + + def _do_scroll_end(self, chat_history: VerticalScroll) -> None: + self._pending_scroll_end = False + try: + chat_history.scroll_end(animate=False) + except Exception: + logger.debug("Failed to scroll chat to end", exc_info=True) def _get_chat_placeholder_content( self, message: str, placeholder_class: str @@ -1292,7 +1301,7 @@ class StrixTUIApp(App): # type: ignore[misc] def _start_dot_animation(self) -> None: if self._dot_animation_timer is None: - self._dot_animation_timer = self.set_interval(0.06, self._animate_dots) + self._dot_animation_timer = self.set_interval(0.25, self._animate_dots) def _stop_dot_animation(self) -> None: if self._dot_animation_timer is not None: diff --git a/strix/interface/tui/renderers/agent_message_renderer.py b/strix/interface/tui/renderers/agent_message_renderer.py index a7085208..b96275a8 100644 --- a/strix/interface/tui/renderers/agent_message_renderer.py +++ b/strix/interface/tui/renderers/agent_message_renderer.py @@ -1,6 +1,6 @@ import re from functools import cache -from typing import Any +from typing import Any, ClassVar from pygments.lexers import get_lexer_by_name, guess_lexer from pygments.styles import get_style_by_name @@ -161,6 +161,8 @@ def _process_inline_formatting(line: str) -> Text: class AgentMessageRenderer: + _cache: ClassVar[dict[int, Text]] = {} + @classmethod def render_simple(cls, content: str) -> Text: if not content: @@ -168,4 +170,12 @@ class AgentMessageRenderer: cleaned = _BLANK_LINE_RUNS.sub("\n\n", content).strip() if not cleaned: return Text() - return _apply_markdown_styles(cleaned) + cache_key = hash(cleaned) + cached = cls._cache.get(cache_key) + if cached is not None: + return cached.copy() + rendered = _apply_markdown_styles(cleaned) + if len(cls._cache) > 100: + cls._cache.clear() + cls._cache[cache_key] = rendered + return rendered