mirror of
https://github.com/usestrix/strix.git
synced 2026-08-16 09:26:39 +02:00
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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user