Guard TUI chat rendering against invalid Rich spans (#375)

This commit is contained in:
Ahmed Allam
2026-03-19 22:28:42 -07:00
committed by GitHub
parent 9a0bc5e491
commit 31d8a09c95

View File

@@ -18,7 +18,7 @@ from rich.align import Align
from rich.console import Group from rich.console import Group
from rich.panel import Panel from rich.panel import Panel
from rich.style import Style from rich.style import Style
from rich.text import Text from rich.text import Span, Text
from textual import events, on from textual import events, on
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.binding import Binding from textual.binding import Binding
@@ -1035,13 +1035,37 @@ class StrixTUIApp(App): # type: ignore[misc]
if i > 0: if i > 0:
combined.append("\n") combined.append("\n")
StrixTUIApp._append_renderable(combined, item) StrixTUIApp._append_renderable(combined, item)
return combined return StrixTUIApp._sanitize_text(combined)
@staticmethod
def _sanitize_text(text: Text) -> Text:
"""Clamp spans so Rich/Textual can't crash on malformed offsets."""
plain = text.plain
text_length = len(plain)
sanitized_spans: list[Span] = []
for span in text.spans:
start = max(0, min(span.start, text_length))
end = max(0, min(span.end, text_length))
if end > start:
sanitized_spans.append(Span(start, end, span.style))
return Text(
plain,
style=text.style,
justify=text.justify,
overflow=text.overflow,
no_wrap=text.no_wrap,
end=text.end,
tab_size=text.tab_size,
spans=sanitized_spans,
)
@staticmethod @staticmethod
def _append_renderable(combined: Text, item: Any) -> None: def _append_renderable(combined: Text, item: Any) -> None:
"""Recursively append a renderable's text content to a combined Text.""" """Recursively append a renderable's text content to a combined Text."""
if isinstance(item, Text): if isinstance(item, Text):
combined.append_text(item) combined.append_text(StrixTUIApp._sanitize_text(item))
elif isinstance(item, Group): elif isinstance(item, Group):
for j, sub in enumerate(item.renderables): for j, sub in enumerate(item.renderables):
if j > 0: if j > 0:
@@ -1086,7 +1110,7 @@ class StrixTUIApp(App): # type: ignore[misc]
return Text() return Text()
if len(renderables) == 1 and isinstance(renderables[0], Text): if len(renderables) == 1 and isinstance(renderables[0], Text):
return renderables[0] return self._sanitize_text(renderables[0])
return self._merge_renderables(renderables) return self._merge_renderables(renderables)
@@ -1122,7 +1146,7 @@ class StrixTUIApp(App): # type: ignore[misc]
if not renderables: if not renderables:
result = Text() result = Text()
elif len(renderables) == 1 and isinstance(renderables[0], Text): elif len(renderables) == 1 and isinstance(renderables[0], Text):
result = renderables[0] result = self._sanitize_text(renderables[0])
else: else:
result = self._merge_renderables(renderables) result = self._merge_renderables(renderables)