Better handling for rich markup errors

This commit is contained in:
Ahmed Allam
2025-09-24 01:13:02 -07:00
parent c8b23720df
commit af01294c46
11 changed files with 39 additions and 31 deletions

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "strix-agent" name = "strix-agent"
version = "0.1.15" version = "0.1.17"
description = "Open-source AI Hackers for your apps" description = "Open-source AI Hackers for your apps"
authors = ["Strix <hi@usestrix.com>"] authors = ["Strix <hi@usestrix.com>"]
readme = "README.md" readme = "README.md"

View File

@@ -9,8 +9,8 @@ import threading
from collections.abc import Callable from collections.abc import Callable
from typing import Any, ClassVar from typing import Any, ClassVar
from rich.markup import MarkupError
from rich.markup import escape as rich_escape from rich.markup import escape as rich_escape
from rich.text import 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
@@ -550,7 +550,9 @@ class StrixCLIApp(App): # type: ignore[misc]
elif status == "llm_failed": elif status == "llm_failed":
error_msg = agent_data.get("error_message", "") error_msg = agent_data.get("error_message", "")
display_msg = ( display_msg = (
f"[red]{error_msg}[/red]" if error_msg else "[red]LLM request failed[/red]" f"[red]{escape_markup(error_msg)}[/red]"
if error_msg
else "[red]LLM request failed[/red]"
) )
self._safe_widget_operation(status_text.update, display_msg) self._safe_widget_operation(status_text.update, display_msg)
self._safe_widget_operation( self._safe_widget_operation(
@@ -954,7 +956,7 @@ class StrixCLIApp(App): # type: ignore[misc]
content = "\n".join(lines) content = "\n".join(lines)
lines = content.split("\n") lines = content.split("\n")
bordered_lines = [f"[{color}]▍[/] {line}" for line in lines] bordered_lines = [f"[{color}]▍[/{color}] {line}" for line in lines]
return "\n".join(bordered_lines) return "\n".join(bordered_lines)
@on(Tree.NodeHighlighted) # type: ignore[misc] @on(Tree.NodeHighlighted) # type: ignore[misc]
@@ -1146,11 +1148,15 @@ class StrixCLIApp(App): # type: ignore[misc]
def _update_static_content_safe(self, widget: Static, content: str) -> None: def _update_static_content_safe(self, widget: Static, content: str) -> None:
try: try:
widget.update(content) widget.update(content)
except MarkupError: except Exception: # noqa: BLE001
try: try:
widget.update(rich_escape(content)) safe_text = Text.from_markup(content)
except Exception: widget.update(safe_text)
widget.update(rich_escape(str(content))) except Exception: # noqa: BLE001
import re
plain_text = re.sub(r"\[.*?\]", "", content)
widget.update(plain_text)
async def run_strix_cli(args: argparse.Namespace) -> None: async def run_strix_cli(args: argparse.Namespace) -> None:

View File

@@ -31,7 +31,7 @@ class CreateAgentRenderer(BaseToolRenderer):
task = args.get("task", "") task = args.get("task", "")
name = args.get("name", "Agent") name = args.get("name", "Agent")
header = f"🤖 [bold #fbbf24]Creating {name}[/]" header = f"🤖 [bold #fbbf24]Creating {cls.escape_markup(name)}[/]"
if task: if task:
task_display = task[:400] + "..." if len(task) > 400 else task task_display = task[:400] + "..." if len(task) > 400 else task

View File

@@ -48,10 +48,10 @@ class BaseToolRenderer(ABC):
@classmethod @classmethod
def get_status_icon(cls, status: str) -> str: def get_status_icon(cls, status: str) -> str:
status_icons = { status_icons = {
"running": "[#f59e0b]●[/] In progress...", "running": "[#f59e0b]●[/#f59e0b] In progress...",
"completed": "[#22c55e]✓[/] Done", "completed": "[#22c55e]✓[/#22c55e] Done",
"failed": "[#dc2626]✗[/] Failed", "failed": "[#dc2626]✗[/#dc2626] Failed",
"error": "[#dc2626]✗[/] Error", "error": "[#dc2626]✗[/#dc2626] Error",
} }
return status_icons.get(status, "[dim]○[/dim] Unknown") return status_icons.get(status, "[dim]○[/dim] Unknown")

View File

@@ -76,7 +76,7 @@ class BrowserRenderer(BaseToolRenderer):
"double_click": "double clicking", "double_click": "double clicking",
"hover": "hovering", "hover": "hovering",
} }
message = action_words[action] message = cls.escape_markup(action_words[action])
return f"{browser_icon} [#06b6d4]{message}[/]" return f"{browser_icon} [#06b6d4]{message}[/]"
@@ -97,9 +97,9 @@ class BrowserRenderer(BaseToolRenderer):
} }
if action in simple_actions: if action in simple_actions:
return f"{browser_icon} [#06b6d4]{simple_actions[action]}[/]" return f"{browser_icon} [#06b6d4]{cls.escape_markup(simple_actions[action])}[/]"
return f"{browser_icon} [#06b6d4]{action}[/]" return f"{browser_icon} [#06b6d4]{cls.escape_markup(action)}[/]"
@classmethod @classmethod
def _format_url(cls, url: str) -> str: def _format_url(cls, url: str) -> str:

View File

@@ -64,7 +64,7 @@ class ViewRequestRenderer(BaseToolRenderer):
part = args.get("part", "request") part = args.get("part", "request")
header = f"👀 [bold #06b6d4]Viewing {part}[/]" header = f"👀 [bold #06b6d4]Viewing {cls.escape_markup(part)}[/]"
if result and isinstance(result, dict): if result and isinstance(result, dict):
if "content" in result: if "content" in result:
@@ -107,7 +107,7 @@ class SendRequestRenderer(BaseToolRenderer):
method = args.get("method", "GET") method = args.get("method", "GET")
url = args.get("url", "") url = args.get("url", "")
header = f"📤 [bold #06b6d4]Sending {method}[/]" header = f"📤 [bold #06b6d4]Sending {cls.escape_markup(method)}[/]"
if result and isinstance(result, dict): if result and isinstance(result, dict):
status_code = result.get("status_code") status_code = result.get("status_code")

View File

@@ -54,7 +54,7 @@ def _render_default_tool_widget(tool_data: dict[str, Any]) -> Static:
status_text = BaseToolRenderer.get_status_icon(status) status_text = BaseToolRenderer.get_status_icon(status)
header = f"→ Using tool [bold blue]{tool_name}[/]" header = f"→ Using tool [bold blue]{BaseToolRenderer.escape_markup(tool_name)}[/]"
content_parts = [header] content_parts = [header]
args_str = BaseToolRenderer.format_args(args) args_str = BaseToolRenderer.format_args(args)

View File

@@ -27,7 +27,7 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
if severity: if severity:
severity_color = cls._get_severity_color(severity.lower()) severity_color = cls._get_severity_color(severity.lower())
content_parts.append( content_parts.append(
f" [dim]Severity: [{severity_color}]{severity.upper()}[/{severity_color}][/]" f" [dim]Severity: [{severity_color}]{cls.escape_markup(severity.upper())}[/{severity_color}][/]"
) )
if content: if content:

View File

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

View File

@@ -111,7 +111,7 @@ class TerminalRenderer(BaseToolRenderer):
) )
if is_special: if is_special:
return f"{terminal_icon} [#ef4444]{command}[/]" return f"{terminal_icon} [#ef4444]{cls.escape_markup(command)}[/]"
if is_input: if is_input:
formatted_command = cls._format_command_display(command) formatted_command = cls._format_command_display(command)

View File

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