From 7d8ffe1e329e8660df8d67a076215622f59196dd Mon Sep 17 00:00:00 2001 From: Ahmed Allam <49919286+0xallam@users.noreply.github.com> Date: Tue, 23 Sep 2025 20:14:08 -0700 Subject: [PATCH] Fix escape issues causing tui to crash (#36) --- strix/cli/app.py | 17 ++++++++++++++--- strix/cli/tool_components/base_renderer.py | 11 ++++++----- strix/cli/tool_components/scan_info_renderer.py | 10 +++++----- .../tool_components/user_message_renderer.py | 8 ++++---- 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/strix/cli/app.py b/strix/cli/app.py index 0e059d0..3953cab 100644 --- a/strix/cli/app.py +++ b/strix/cli/app.py @@ -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) diff --git a/strix/cli/tool_components/base_renderer.py b/strix/cli/tool_components/base_renderer.py index c8a7425..baa32c3 100644 --- a/strix/cli/tool_components/base_renderer.py +++ b/strix/cli/tool_components/base_renderer.py @@ -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") diff --git a/strix/cli/tool_components/scan_info_renderer.py b/strix/cli/tool_components/scan_info_renderer.py index 8bd168c..22a438d 100644 --- a/strix/cli/tool_components/scan_info_renderer.py +++ b/strix/cli/tool_components/scan_info_renderer.py @@ -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) diff --git a/strix/cli/tool_components/user_message_renderer.py b/strix/cli/tool_components/user_message_renderer.py index 4494575..149718d 100644 --- a/strix/cli/tool_components/user_message_renderer.py +++ b/strix/cli/tool_components/user_message_renderer.py @@ -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}"