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.
This commit is contained in:
Ahmed Allam
2025-12-14 10:53:07 -08:00
committed by GitHub
parent 2b926c733b
commit 624f1ed77f
4 changed files with 80 additions and 5 deletions

View File

@@ -10,8 +10,8 @@ You follow all instructions and rules provided to you exactly as written in the
<communication_rules>
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

View File

@@ -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",

View File

@@ -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"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)", r"[italic]\1[/italic]", text)
text = re.sub(r"(?<![_\w])_(?!_)(.+?)(?<!_)_(?![_\w])", r"[italic]\1[/italic]", text)
# Inline code
text = re.sub(r"`([^`]+)`", r"[bold dim]\1[/bold dim]", text)
# Strikethrough
return re.sub(r"~~(.+?)~~", r"[strike]\1[/strike]", text)
@register_tool_renderer
class AgentMessageRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "agent_message"
css_classes: ClassVar[list[str]] = ["chat-message", "agent-message"]
@classmethod
def render(cls, message_data: dict[str, Any]) -> 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)

View File

@@ -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")