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
This commit is contained in:
Hardik-369
2026-07-10 06:55:38 -07:00
committed by Ahmed Allam
parent 9f6d0b106b
commit dd29d99b85
2 changed files with 26 additions and 7 deletions
+14 -5
View File
@@ -128,7 +128,7 @@ class SplashScreen(Static): # type: ignore[misc]
yield panel_static yield panel_static
def on_mount(self) -> None: 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: def on_unmount(self) -> None:
if self._animation_timer is not None: if self._animation_timer is not None:
@@ -729,6 +729,7 @@ class StrixTUIApp(App): # type: ignore[misc]
"#86efac", # Brightest "#86efac", # Brightest
] ]
self._dot_animation_timer: Any | None = None self._dot_animation_timer: Any | None = None
self._pending_scroll_end = False
self._setup_cleanup_handlers() self._setup_cleanup_handlers()
@@ -872,7 +873,7 @@ class StrixTUIApp(App): # type: ignore[misc]
self._start_scan_thread() 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: def _update_ui(self) -> None:
if self.show_splash: if self.show_splash:
@@ -1018,8 +1019,16 @@ class StrixTUIApp(App): # type: ignore[misc]
self._safe_widget_operation(chat_display.update, content) self._safe_widget_operation(chat_display.update, content)
chat_display.set_classes(css_class) chat_display.set_classes(css_class)
if is_at_bottom: if is_at_bottom and not self._pending_scroll_end:
self.call_later(chat_history.scroll_end, animate=False) 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( def _get_chat_placeholder_content(
self, message: str, placeholder_class: str self, message: str, placeholder_class: str
@@ -1292,7 +1301,7 @@ class StrixTUIApp(App): # type: ignore[misc]
def _start_dot_animation(self) -> None: def _start_dot_animation(self) -> None:
if self._dot_animation_timer is 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: def _stop_dot_animation(self) -> None:
if self._dot_animation_timer is not None: if self._dot_animation_timer is not None:
@@ -1,6 +1,6 @@
import re import re
from functools import cache 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.lexers import get_lexer_by_name, guess_lexer
from pygments.styles import get_style_by_name from pygments.styles import get_style_by_name
@@ -161,6 +161,8 @@ def _process_inline_formatting(line: str) -> Text:
class AgentMessageRenderer: class AgentMessageRenderer:
_cache: ClassVar[dict[int, Text]] = {}
@classmethod @classmethod
def render_simple(cls, content: str) -> Text: def render_simple(cls, content: str) -> Text:
if not content: if not content:
@@ -168,4 +170,12 @@ class AgentMessageRenderer:
cleaned = _BLANK_LINE_RUNS.sub("\n\n", content).strip() cleaned = _BLANK_LINE_RUNS.sub("\n\n", content).strip()
if not cleaned: if not cleaned:
return Text() 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