Fix escape issues causing tui to crash (#36)

This commit is contained in:
Ahmed Allam
2025-09-23 20:14:08 -07:00
committed by GitHub
parent aabf97af0a
commit 7d8ffe1e32
4 changed files with 29 additions and 17 deletions

View File

@@ -9,6 +9,8 @@ import threading
from collections.abc import Callable
from typing import Any, ClassVar
from rich.markup import MarkupError
from rich.markup import escape as rich_escape
from textual import events, on
from textual.app import App, ComposeResult
from textual.binding import Binding
@@ -24,7 +26,7 @@ from strix.llm.config import LLMConfig
def escape_markup(text: str) -> str:
return text.replace("[", "\\[").replace("]", "\\]")
return rich_escape(text)
class ChatTextArea(TextArea): # type: ignore[misc]
@@ -483,7 +485,7 @@ class StrixCLIApp(App): # type: ignore[misc]
self._displayed_events = current_event_ids
chat_display = self.query_one("#chat_display", Static)
chat_display.update(content)
self._update_static_content_safe(chat_display, content)
chat_display.set_classes(css_class)
@@ -952,7 +954,7 @@ class StrixCLIApp(App): # type: ignore[misc]
content = "\n".join(lines)
lines = content.split("\n")
bordered_lines = [f"[{color}]▍[/{color}] {line}" for line in lines]
bordered_lines = [f"[{color}]▍[/] {line}" for line in lines]
return "\n".join(bordered_lines)
@on(Tree.NodeHighlighted) # type: ignore[misc]
@@ -1141,6 +1143,15 @@ class StrixCLIApp(App): # type: ignore[misc]
else:
return True
def _update_static_content_safe(self, widget: Static, content: str) -> None:
try:
widget.update(content)
except MarkupError:
try:
widget.update(rich_escape(content))
except Exception:
widget.update(rich_escape(str(content)))
async def run_strix_cli(args: argparse.Namespace) -> None:
app = StrixCLIApp(args)

View File

@@ -1,6 +1,7 @@
from abc import ABC, abstractmethod
from typing import Any, ClassVar
from rich.markup import escape as rich_escape
from textual.widgets import Static
@@ -16,7 +17,7 @@ class BaseToolRenderer(ABC):
@classmethod
def escape_markup(cls, text: str) -> str:
return text.replace("[", "\\[").replace("]", "\\]")
return rich_escape(text)
@classmethod
def format_args(cls, args: dict[str, Any], max_length: int = 500) -> str:
@@ -47,10 +48,10 @@ class BaseToolRenderer(ABC):
@classmethod
def get_status_icon(cls, status: str) -> str:
status_icons = {
"running": "[#f59e0b]●[/#f59e0b] In progress...",
"completed": "[#22c55e]✓[/#22c55e] Done",
"failed": "[#dc2626]✗[/#dc2626] Failed",
"error": "[#dc2626]✗[/#dc2626] Error",
"running": "[#f59e0b]●[/] In progress...",
"completed": "[#22c55e]✓[/] Done",
"failed": "[#dc2626]✗[/] Failed",
"error": "[#dc2626]✗[/] Error",
}
return status_icons.get(status, "[dim]○[/dim] Unknown")

View File

@@ -28,11 +28,11 @@ class ScanStartInfoRenderer(BaseToolRenderer):
@classmethod
def _build_target_display(cls, target: dict[str, Any]) -> str:
if target_url := target.get("target_url"):
return f"[bold #22c55e]{target_url}[/]"
return f"[bold #22c55e]{cls.escape_markup(target_url)}[/]"
if target_repo := target.get("target_repo"):
return f"[bold #22c55e]{target_repo}[/]"
return f"[bold #22c55e]{cls.escape_markup(target_repo)}[/]"
if target_path := target.get("target_path"):
return f"[bold #22c55e]{target_path}[/]"
return f"[bold #22c55e]{cls.escape_markup(target_path)}[/]"
return "[dim]unknown target[/dim]"
@@ -49,9 +49,9 @@ class SubagentStartInfoRenderer(BaseToolRenderer):
name = args.get("name", "Unknown Agent")
task = args.get("task", "")
content = f"🤖 Spawned subagent [bold #22c55e]{name}[/]"
content = f"🤖 Spawned subagent [bold #22c55e]{cls.escape_markup(name)}[/]"
if task:
content += f"\n Task: [dim]{task}[/dim]"
content += f"\n Task: [dim]{cls.escape_markup(task)}[/dim]"
css_classes = cls.get_css_classes(status)
return Static(content, classes=css_classes)

View File

@@ -22,9 +22,9 @@ class UserMessageRenderer(BaseToolRenderer):
content = content[:297] + "..."
lines = content.split("\n")
bordered_lines = [f"[#3b82f6]▍[/#3b82f6] {line}" for line in lines]
bordered_lines = [f"[#3b82f6]▍[/] {line}" for line in lines]
bordered_content = "\n".join(bordered_lines)
formatted_content = f"[#3b82f6]▍[/#3b82f6] [bold]You:[/]\n{bordered_content}"
formatted_content = f"[#3b82f6]▍[/] [bold]You:[/]\n{bordered_content}"
css_classes = " ".join(cls.css_classes)
return Static(formatted_content, classes=css_classes)
@@ -38,6 +38,6 @@ class UserMessageRenderer(BaseToolRenderer):
content = content[:297] + "..."
lines = content.split("\n")
bordered_lines = [f"[#3b82f6]▍[/#3b82f6] {line}" for line in lines]
bordered_lines = [f"[#3b82f6]▍[/] {line}" for line in lines]
bordered_content = "\n".join(bordered_lines)
return f"[#3b82f6]▍[/#3b82f6] [bold]You:[/]\n{bordered_content}"
return f"[#3b82f6]▍[/] [bold]You:[/]\n{bordered_content}"