Fix escape issues causing tui to crash (#36)
This commit is contained in:
@@ -9,6 +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 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
|
||||||
@@ -24,7 +26,7 @@ from strix.llm.config import LLMConfig
|
|||||||
|
|
||||||
|
|
||||||
def escape_markup(text: str) -> str:
|
def escape_markup(text: str) -> str:
|
||||||
return text.replace("[", "\\[").replace("]", "\\]")
|
return rich_escape(text)
|
||||||
|
|
||||||
|
|
||||||
class ChatTextArea(TextArea): # type: ignore[misc]
|
class ChatTextArea(TextArea): # type: ignore[misc]
|
||||||
@@ -483,7 +485,7 @@ class StrixCLIApp(App): # type: ignore[misc]
|
|||||||
self._displayed_events = current_event_ids
|
self._displayed_events = current_event_ids
|
||||||
|
|
||||||
chat_display = self.query_one("#chat_display", Static)
|
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)
|
chat_display.set_classes(css_class)
|
||||||
|
|
||||||
@@ -952,7 +954,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}]▍[/{color}] {line}" for line in lines]
|
bordered_lines = [f"[{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]
|
||||||
@@ -1141,6 +1143,15 @@ class StrixCLIApp(App): # type: ignore[misc]
|
|||||||
else:
|
else:
|
||||||
return True
|
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:
|
async def run_strix_cli(args: argparse.Namespace) -> None:
|
||||||
app = StrixCLIApp(args)
|
app = StrixCLIApp(args)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
|
from rich.markup import escape as rich_escape
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ class BaseToolRenderer(ABC):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def escape_markup(cls, text: str) -> str:
|
def escape_markup(cls, text: str) -> str:
|
||||||
return text.replace("[", "\\[").replace("]", "\\]")
|
return rich_escape(text)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def format_args(cls, args: dict[str, Any], max_length: int = 500) -> str:
|
def format_args(cls, args: dict[str, Any], max_length: int = 500) -> str:
|
||||||
@@ -47,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]●[/#f59e0b] In progress...",
|
"running": "[#f59e0b]●[/] In progress...",
|
||||||
"completed": "[#22c55e]✓[/#22c55e] Done",
|
"completed": "[#22c55e]✓[/] Done",
|
||||||
"failed": "[#dc2626]✗[/#dc2626] Failed",
|
"failed": "[#dc2626]✗[/] Failed",
|
||||||
"error": "[#dc2626]✗[/#dc2626] Error",
|
"error": "[#dc2626]✗[/] Error",
|
||||||
}
|
}
|
||||||
return status_icons.get(status, "[dim]○[/dim] Unknown")
|
return status_icons.get(status, "[dim]○[/dim] Unknown")
|
||||||
|
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ 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]{target_url}[/]"
|
return f"[bold #22c55e]{cls.escape_markup(target_url)}[/]"
|
||||||
if target_repo := target.get("target_repo"):
|
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"):
|
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]"
|
return "[dim]unknown target[/dim]"
|
||||||
|
|
||||||
|
|
||||||
@@ -49,9 +49,9 @@ 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]{name}[/]"
|
content = f"🤖 Spawned subagent [bold #22c55e]{cls.escape_markup(name)}[/]"
|
||||||
if task:
|
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)
|
css_classes = cls.get_css_classes(status)
|
||||||
return Static(content, classes=css_classes)
|
return Static(content, classes=css_classes)
|
||||||
|
|||||||
@@ -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]▍[/#3b82f6] {line}" for line in lines]
|
bordered_lines = [f"[#3b82f6]▍[/] {line}" for line in lines]
|
||||||
bordered_content = "\n".join(bordered_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)
|
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]▍[/#3b82f6] {line}" for line in lines]
|
bordered_lines = [f"[#3b82f6]▍[/] {line}" for line in lines]
|
||||||
bordered_content = "\n".join(bordered_lines)
|
bordered_content = "\n".join(bordered_lines)
|
||||||
return f"[#3b82f6]▍[/#3b82f6] [bold]You:[/]\n{bordered_content}"
|
return f"[#3b82f6]▍[/] [bold]You:[/]\n{bordered_content}"
|
||||||
|
|||||||
Reference in New Issue
Block a user