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:
@@ -10,8 +10,8 @@ You follow all instructions and rules provided to you exactly as written in the
|
|||||||
|
|
||||||
<communication_rules>
|
<communication_rules>
|
||||||
CLI OUTPUT:
|
CLI OUTPUT:
|
||||||
- Never use markdown formatting - you are a CLI agent
|
- You may use simple markdown: **bold**, *italic*, `code`, ~~strikethrough~~, [links](url), and # headers
|
||||||
- Output plain text only (no **bold**, `code`, [links], # headers)
|
- Do NOT use complex markdown like bullet lists, numbered lists, or tables
|
||||||
- Use line breaks and indentation for structure
|
- Use line breaks and indentation for structure
|
||||||
- NEVER use "Strix" or any identifiable names/markers in HTTP requests, payloads, user-agents, or any inputs
|
- NEVER use "Strix" or any identifiable names/markers in HTTP requests, payloads, user-agents, or any inputs
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from . import (
|
from . import (
|
||||||
|
agent_message_renderer,
|
||||||
agents_graph_renderer,
|
agents_graph_renderer,
|
||||||
browser_renderer,
|
browser_renderer,
|
||||||
file_edit_renderer,
|
file_edit_renderer,
|
||||||
@@ -21,6 +22,7 @@ from .registry import ToolTUIRegistry, get_tool_renderer, register_tool_renderer
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"BaseToolRenderer",
|
"BaseToolRenderer",
|
||||||
"ToolTUIRegistry",
|
"ToolTUIRegistry",
|
||||||
|
"agent_message_renderer",
|
||||||
"agents_graph_renderer",
|
"agents_graph_renderer",
|
||||||
"browser_renderer",
|
"browser_renderer",
|
||||||
"file_edit_renderer",
|
"file_edit_renderer",
|
||||||
|
|||||||
70
strix/interface/tool_components/agent_message_renderer.py
Normal file
70
strix/interface/tool_components/agent_message_renderer.py
Normal 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)
|
||||||
@@ -987,7 +987,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
|
|
||||||
def _render_chat_content(self, msg_data: dict[str, Any]) -> str:
|
def _render_chat_content(self, msg_data: dict[str, Any]) -> str:
|
||||||
role = msg_data.get("role")
|
role = msg_data.get("role")
|
||||||
content = escape_markup(msg_data.get("content", ""))
|
content = msg_data.get("content", "")
|
||||||
|
|
||||||
if not content:
|
if not content:
|
||||||
return ""
|
return ""
|
||||||
@@ -995,8 +995,11 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
if role == "user":
|
if role == "user":
|
||||||
from strix.interface.tool_components.user_message_renderer import UserMessageRenderer
|
from strix.interface.tool_components.user_message_renderer import UserMessageRenderer
|
||||||
|
|
||||||
return UserMessageRenderer.render_simple(content)
|
return UserMessageRenderer.render_simple(escape_markup(content))
|
||||||
return 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:
|
def _render_tool_content_simple(self, tool_data: dict[str, Any]) -> str:
|
||||||
tool_name = tool_data.get("tool_name", "Unknown Tool")
|
tool_name = tool_data.get("tool_name", "Unknown Tool")
|
||||||
|
|||||||
Reference in New Issue
Block a user