From 624f1ed77f1221419bdec37c8e2a44f33f8fdb89 Mon Sep 17 00:00:00 2001 From: Ahmed Allam <49919286+0xallam@users.noreply.github.com> Date: Sun, 14 Dec 2025 10:53:07 -0800 Subject: [PATCH] feat(tui): add markdown rendering for agent messages (#197) Add AgentMessageRenderer to render agent messages with basic markdown support: - Headers (#, ##, ###, ####) - Bold (**text**) and italic (*text*) - Inline code and fenced code blocks - Links [text](url) and strikethrough Update system prompt to allow agents to use simple markdown formatting. --- strix/agents/StrixAgent/system_prompt.jinja | 4 +- strix/interface/tool_components/__init__.py | 2 + .../tool_components/agent_message_renderer.py | 70 +++++++++++++++++++ strix/interface/tui.py | 9 ++- 4 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 strix/interface/tool_components/agent_message_renderer.py diff --git a/strix/agents/StrixAgent/system_prompt.jinja b/strix/agents/StrixAgent/system_prompt.jinja index ab23e56..276a0fe 100644 --- a/strix/agents/StrixAgent/system_prompt.jinja +++ b/strix/agents/StrixAgent/system_prompt.jinja @@ -10,8 +10,8 @@ You follow all instructions and rules provided to you exactly as written in the CLI OUTPUT: -- Never use markdown formatting - you are a CLI agent -- Output plain text only (no **bold**, `code`, [links], # headers) +- You may use simple markdown: **bold**, *italic*, `code`, ~~strikethrough~~, [links](url), and # headers +- Do NOT use complex markdown like bullet lists, numbered lists, or tables - Use line breaks and indentation for structure - NEVER use "Strix" or any identifiable names/markers in HTTP requests, payloads, user-agents, or any inputs diff --git a/strix/interface/tool_components/__init__.py b/strix/interface/tool_components/__init__.py index 10f0f47..cb8aeea 100644 --- a/strix/interface/tool_components/__init__.py +++ b/strix/interface/tool_components/__init__.py @@ -1,4 +1,5 @@ from . import ( + agent_message_renderer, agents_graph_renderer, browser_renderer, file_edit_renderer, @@ -21,6 +22,7 @@ from .registry import ToolTUIRegistry, get_tool_renderer, register_tool_renderer __all__ = [ "BaseToolRenderer", "ToolTUIRegistry", + "agent_message_renderer", "agents_graph_renderer", "browser_renderer", "file_edit_renderer", diff --git a/strix/interface/tool_components/agent_message_renderer.py b/strix/interface/tool_components/agent_message_renderer.py new file mode 100644 index 0000000..a4fb72e --- /dev/null +++ b/strix/interface/tool_components/agent_message_renderer.py @@ -0,0 +1,70 @@ +import re +from typing import Any, ClassVar + +from textual.widgets import Static + +from .base_renderer import BaseToolRenderer +from .registry import register_tool_renderer + + +def markdown_to_rich(text: str) -> str: + # Fenced code blocks: ```lang\n...\n``` or ```\n...\n``` + text = re.sub( + r"```(?:\w*)\n(.*?)```", + r"[dim]\1[/dim]", + text, + flags=re.DOTALL, + ) + + # Headers + text = re.sub(r"^#### (.+)$", r"[bold]\1[/bold]", text, flags=re.MULTILINE) + text = re.sub(r"^### (.+)$", r"[bold]\1[/bold]", text, flags=re.MULTILINE) + text = re.sub(r"^## (.+)$", r"[bold]\1[/bold]", text, flags=re.MULTILINE) + text = re.sub(r"^# (.+)$", r"[bold]\1[/bold]", text, flags=re.MULTILINE) + + # Links + text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"[underline]\1[/underline] [dim](\2)[/dim]", text) + + # Bold + text = re.sub(r"\*\*(.+?)\*\*", r"[bold]\1[/bold]", text) + text = re.sub(r"__(.+?)__", r"[bold]\1[/bold]", text) + + # Italic + text = re.sub(r"(? Static: + content = message_data.get("content", "") + + if not content: + return Static("", classes=cls.css_classes) + + formatted_content = cls._format_agent_message(content) + + css_classes = " ".join(cls.css_classes) + return Static(formatted_content, classes=css_classes) + + @classmethod + def render_simple(cls, content: str) -> str: + if not content: + return "" + + return cls._format_agent_message(content) + + @classmethod + def _format_agent_message(cls, content: str) -> str: + escaped_content = cls.escape_markup(content) + return markdown_to_rich(escaped_content) diff --git a/strix/interface/tui.py b/strix/interface/tui.py index 58fa120..b2d3bf0 100644 --- a/strix/interface/tui.py +++ b/strix/interface/tui.py @@ -987,7 +987,7 @@ class StrixTUIApp(App): # type: ignore[misc] def _render_chat_content(self, msg_data: dict[str, Any]) -> str: role = msg_data.get("role") - content = escape_markup(msg_data.get("content", "")) + content = msg_data.get("content", "") if not content: return "" @@ -995,8 +995,11 @@ class StrixTUIApp(App): # type: ignore[misc] if role == "user": from strix.interface.tool_components.user_message_renderer import UserMessageRenderer - return UserMessageRenderer.render_simple(content) - return content + return UserMessageRenderer.render_simple(escape_markup(content)) + + from strix.interface.tool_components.agent_message_renderer import AgentMessageRenderer + + return AgentMessageRenderer.render_simple(content) def _render_tool_content_simple(self, tool_data: dict[str, Any]) -> str: tool_name = tool_data.get("tool_name", "Unknown Tool")