feat(tui): refactor TUI components for improved text rendering and styling
- Removed unused escape_markup function and integrated rich.text for better text handling. - Updated various renderers to utilize Text for consistent styling and formatting. - Enhanced chat and agent message displays with dynamic text features. - Improved error handling and display for various tool components. - Refined TUI styles for better visual consistency across components.
This commit is contained in:
@@ -48,7 +48,7 @@ Screen {
|
|||||||
#agents_tree {
|
#agents_tree {
|
||||||
height: 1fr;
|
height: 1fr;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: round #1a1a1a;
|
border: round #333333;
|
||||||
border-title-color: #a8a29e;
|
border-title-color: #a8a29e;
|
||||||
border-title-style: bold;
|
border-title-style: bold;
|
||||||
padding: 1;
|
padding: 1;
|
||||||
|
|||||||
@@ -1,43 +1,163 @@
|
|||||||
import re
|
from functools import cache
|
||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
|
from pygments.lexers import get_lexer_by_name, guess_lexer
|
||||||
|
from pygments.styles import get_style_by_name
|
||||||
|
from pygments.util import ClassNotFound
|
||||||
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
from .base_renderer import BaseToolRenderer
|
from .base_renderer import BaseToolRenderer
|
||||||
from .registry import register_tool_renderer
|
from .registry import register_tool_renderer
|
||||||
|
|
||||||
|
|
||||||
def markdown_to_rich(text: str) -> str:
|
_HEADER_STYLES = [
|
||||||
# Fenced code blocks: ```lang\n...\n``` or ```\n...\n```
|
("###### ", 7, "bold #4ade80"),
|
||||||
text = re.sub(
|
("##### ", 6, "bold #22c55e"),
|
||||||
r"```(?:\w*)\n(.*?)```",
|
("#### ", 5, "bold #16a34a"),
|
||||||
r"[dim]\1[/dim]",
|
("### ", 4, "bold #15803d"),
|
||||||
text,
|
("## ", 3, "bold #22c55e"),
|
||||||
flags=re.DOTALL,
|
("# ", 2, "bold #4ade80"),
|
||||||
)
|
]
|
||||||
|
|
||||||
# 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
|
@cache
|
||||||
text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"[underline]\1[/underline] [dim](\2)[/dim]", text)
|
def _get_style_colors() -> dict[Any, str]:
|
||||||
|
style = get_style_by_name("native")
|
||||||
|
return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]}
|
||||||
|
|
||||||
# Bold
|
|
||||||
text = re.sub(r"\*\*(.+?)\*\*", r"[bold]\1[/bold]", text)
|
|
||||||
text = re.sub(r"__(.+?)__", r"[bold]\1[/bold]", text)
|
|
||||||
|
|
||||||
# Italic
|
def _get_token_color(token_type: Any) -> str | None:
|
||||||
text = re.sub(r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)", r"[italic]\1[/italic]", text)
|
colors = _get_style_colors()
|
||||||
text = re.sub(r"(?<![_\w])_(?!_)(.+?)(?<!_)_(?![_\w])", r"[italic]\1[/italic]", text)
|
while token_type:
|
||||||
|
if token_type in colors:
|
||||||
|
return colors[token_type]
|
||||||
|
token_type = token_type.parent
|
||||||
|
return None
|
||||||
|
|
||||||
# Inline code
|
|
||||||
text = re.sub(r"`([^`]+)`", r"[bold dim]\1[/bold dim]", text)
|
|
||||||
|
|
||||||
# Strikethrough
|
def _highlight_code(code: str, language: str | None = None) -> Text:
|
||||||
return re.sub(r"~~(.+?)~~", r"[strike]\1[/strike]", text)
|
text = Text()
|
||||||
|
|
||||||
|
try:
|
||||||
|
lexer = get_lexer_by_name(language) if language else guess_lexer(code)
|
||||||
|
except ClassNotFound:
|
||||||
|
text.append(code, style="#d4d4d4")
|
||||||
|
return text
|
||||||
|
|
||||||
|
for token_type, token_value in lexer.get_tokens(code):
|
||||||
|
if not token_value:
|
||||||
|
continue
|
||||||
|
color = _get_token_color(token_type)
|
||||||
|
text.append(token_value, style=color)
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _try_parse_header(line: str) -> tuple[str, str] | None:
|
||||||
|
for prefix, strip_len, style in _HEADER_STYLES:
|
||||||
|
if line.startswith(prefix):
|
||||||
|
return (line[strip_len:], style)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_markdown_styles(text: str) -> Text: # noqa: PLR0912
|
||||||
|
result = Text()
|
||||||
|
lines = text.split("\n")
|
||||||
|
|
||||||
|
in_code_block = False
|
||||||
|
code_block_lang: str | None = None
|
||||||
|
code_block_lines: list[str] = []
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if i > 0 and not in_code_block:
|
||||||
|
result.append("\n")
|
||||||
|
|
||||||
|
if line.startswith("```"):
|
||||||
|
if not in_code_block:
|
||||||
|
in_code_block = True
|
||||||
|
code_block_lang = line[3:].strip() or None
|
||||||
|
code_block_lines = []
|
||||||
|
if i > 0:
|
||||||
|
result.append("\n")
|
||||||
|
else:
|
||||||
|
in_code_block = False
|
||||||
|
code_content = "\n".join(code_block_lines)
|
||||||
|
if code_content:
|
||||||
|
result.append_text(_highlight_code(code_content, code_block_lang))
|
||||||
|
code_block_lines = []
|
||||||
|
code_block_lang = None
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_code_block:
|
||||||
|
code_block_lines.append(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
header = _try_parse_header(line)
|
||||||
|
if header:
|
||||||
|
result.append(header[0], style=header[1])
|
||||||
|
elif line.startswith("> "):
|
||||||
|
result.append("┃ ", style="#22c55e")
|
||||||
|
result.append_text(_process_inline_formatting(line[2:]))
|
||||||
|
elif line.startswith(("- ", "* ")):
|
||||||
|
result.append("• ", style="#22c55e")
|
||||||
|
result.append_text(_process_inline_formatting(line[2:]))
|
||||||
|
elif len(line) > 2 and line[0].isdigit() and line[1:3] in (". ", ") "):
|
||||||
|
result.append(line[0] + ". ", style="#22c55e")
|
||||||
|
result.append_text(_process_inline_formatting(line[2:]))
|
||||||
|
elif line.strip() in ("---", "***", "___"):
|
||||||
|
result.append("─" * 40, style="#22c55e")
|
||||||
|
else:
|
||||||
|
result.append_text(_process_inline_formatting(line))
|
||||||
|
|
||||||
|
if in_code_block and code_block_lines:
|
||||||
|
code_content = "\n".join(code_block_lines)
|
||||||
|
result.append_text(_highlight_code(code_content, code_block_lang))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _process_inline_formatting(line: str) -> Text:
|
||||||
|
result = Text()
|
||||||
|
i = 0
|
||||||
|
n = len(line)
|
||||||
|
|
||||||
|
while i < n:
|
||||||
|
if i + 1 < n and line[i : i + 2] in ("**", "__"):
|
||||||
|
marker = line[i : i + 2]
|
||||||
|
end = line.find(marker, i + 2)
|
||||||
|
if end != -1:
|
||||||
|
result.append(line[i + 2 : end], style="bold #4ade80")
|
||||||
|
i = end + 2
|
||||||
|
continue
|
||||||
|
|
||||||
|
if i + 1 < n and line[i : i + 2] == "~~":
|
||||||
|
end = line.find("~~", i + 2)
|
||||||
|
if end != -1:
|
||||||
|
result.append(line[i + 2 : end], style="strike #525252")
|
||||||
|
i = end + 2
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line[i] == "`":
|
||||||
|
end = line.find("`", i + 1)
|
||||||
|
if end != -1:
|
||||||
|
result.append(line[i + 1 : end], style="bold #22c55e on #0a0a0a")
|
||||||
|
i = end + 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line[i] in ("*", "_"):
|
||||||
|
marker = line[i]
|
||||||
|
if i + 1 < n and line[i + 1] != marker:
|
||||||
|
end = line.find(marker, i + 1)
|
||||||
|
if end != -1 and (end + 1 >= n or line[end + 1] != marker):
|
||||||
|
result.append(line[i + 1 : end], style="italic #86efac")
|
||||||
|
i = end + 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
result.append(line[i])
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -46,25 +166,19 @@ class AgentMessageRenderer(BaseToolRenderer):
|
|||||||
css_classes: ClassVar[list[str]] = ["chat-message", "agent-message"]
|
css_classes: ClassVar[list[str]] = ["chat-message", "agent-message"]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, message_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
content = message_data.get("content", "")
|
content = tool_data.get("content", "")
|
||||||
|
|
||||||
if not content:
|
if not content:
|
||||||
return Static("", classes=cls.css_classes)
|
return Static(Text(), classes=" ".join(cls.css_classes))
|
||||||
|
|
||||||
formatted_content = cls._format_agent_message(content)
|
styled_text = _apply_markdown_styles(content)
|
||||||
|
|
||||||
css_classes = " ".join(cls.css_classes)
|
return Static(styled_text, classes=" ".join(cls.css_classes))
|
||||||
return Static(formatted_content, classes=css_classes)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render_simple(cls, content: str) -> str:
|
def render_simple(cls, content: str) -> Text:
|
||||||
if not content:
|
if not content:
|
||||||
return ""
|
return Text()
|
||||||
|
|
||||||
return cls._format_agent_message(content)
|
return _apply_markdown_styles(content)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _format_agent_message(cls, content: str) -> str:
|
|
||||||
escaped_content = cls.escape_markup(content)
|
|
||||||
return markdown_to_rich(escaped_content)
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
from .base_renderer import BaseToolRenderer
|
from .base_renderer import BaseToolRenderer
|
||||||
@@ -13,10 +14,12 @@ class ViewAgentGraphRenderer(BaseToolRenderer):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
|
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
|
||||||
content_text = "🕸️ [bold #fbbf24]Viewing agents graph[/]"
|
text = Text()
|
||||||
|
text.append("🕸️ ")
|
||||||
|
text.append("Viewing agents graph", style="bold #fbbf24")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -31,16 +34,19 @@ 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 {cls.escape_markup(name)}[/]"
|
text = Text()
|
||||||
|
text.append("🤖 ")
|
||||||
|
text.append(f"Creating {name}", style="bold #fbbf24")
|
||||||
|
|
||||||
if task:
|
if task:
|
||||||
task_display = task[:400] + "..." if len(task) > 400 else task
|
text.append("\n ")
|
||||||
content_text = f"{header}\n [dim]{cls.escape_markup(task_display)}[/]"
|
text.append(cls.truncate(task, 400), style="dim")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Spawning agent...[/]"
|
text.append("\n ")
|
||||||
|
text.append("Spawning agent...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -54,16 +60,19 @@ class SendMessageToAgentRenderer(BaseToolRenderer):
|
|||||||
|
|
||||||
message = args.get("message", "")
|
message = args.get("message", "")
|
||||||
|
|
||||||
header = "💬 [bold #fbbf24]Sending message[/]"
|
text = Text()
|
||||||
|
text.append("💬 ")
|
||||||
|
text.append("Sending message", style="bold #fbbf24")
|
||||||
|
|
||||||
if message:
|
if message:
|
||||||
message_display = message[:400] + "..." if len(message) > 400 else message
|
text.append("\n ")
|
||||||
content_text = f"{header}\n [dim]{cls.escape_markup(message_display)}[/]"
|
text.append(cls.truncate(message, 400), style="dim")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Sending...[/]"
|
text.append("\n ")
|
||||||
|
text.append("Sending...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -79,25 +88,28 @@ class AgentFinishRenderer(BaseToolRenderer):
|
|||||||
findings = args.get("findings", [])
|
findings = args.get("findings", [])
|
||||||
success = args.get("success", True)
|
success = args.get("success", True)
|
||||||
|
|
||||||
header = (
|
text = Text()
|
||||||
"🏁 [bold #fbbf24]Agent completed[/]" if success else "🏁 [bold #fbbf24]Agent failed[/]"
|
text.append("🏁 ")
|
||||||
)
|
|
||||||
|
if success:
|
||||||
|
text.append("Agent completed", style="bold #fbbf24")
|
||||||
|
else:
|
||||||
|
text.append("Agent failed", style="bold #fbbf24")
|
||||||
|
|
||||||
if result_summary:
|
if result_summary:
|
||||||
content_parts = [f"{header}\n [bold]{cls.escape_markup(result_summary)}[/]"]
|
text.append("\n ")
|
||||||
|
text.append(result_summary, style="bold")
|
||||||
|
|
||||||
if findings and isinstance(findings, list):
|
if findings and isinstance(findings, list):
|
||||||
finding_lines = [f"• {finding}" for finding in findings]
|
for finding in findings:
|
||||||
content_parts.append(
|
text.append("\n • ")
|
||||||
f" [dim]{chr(10).join([cls.escape_markup(line) for line in finding_lines])}[/]"
|
text.append(str(finding), style="dim")
|
||||||
)
|
|
||||||
|
|
||||||
content_text = "\n".join(content_parts)
|
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Completing task...[/]"
|
text.append("\n ")
|
||||||
|
text.append("Completing task...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -111,13 +123,16 @@ class WaitForMessageRenderer(BaseToolRenderer):
|
|||||||
|
|
||||||
reason = args.get("reason", "Waiting for messages from other agents or user input")
|
reason = args.get("reason", "Waiting for messages from other agents or user input")
|
||||||
|
|
||||||
header = "⏸️ [bold #fbbf24]Waiting for messages[/]"
|
text = Text()
|
||||||
|
text.append("⏸️ ")
|
||||||
|
text.append("Waiting for messages", style="bold #fbbf24")
|
||||||
|
|
||||||
if reason:
|
if reason:
|
||||||
reason_display = reason[:400] + "..." if len(reason) > 400 else reason
|
text.append("\n ")
|
||||||
content_text = f"{header}\n [dim]{cls.escape_markup(reason_display)}[/]"
|
text.append(cls.truncate(reason, 400), style="dim")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Agent paused until message received...[/]"
|
text.append("\n ")
|
||||||
|
text.append("Agent paused until message received...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any, ClassVar, cast
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
from rich.markup import escape as rich_escape
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
class BaseToolRenderer(ABC):
|
class BaseToolRenderer(ABC):
|
||||||
tool_name: ClassVar[str] = ""
|
tool_name: ClassVar[str] = ""
|
||||||
|
|
||||||
css_classes: ClassVar[list[str]] = ["tool-call"]
|
css_classes: ClassVar[list[str]] = ["tool-call"]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -16,47 +15,86 @@ class BaseToolRenderer(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def escape_markup(cls, text: str) -> str:
|
def build_text(cls, tool_data: dict[str, Any]) -> Text: # noqa: ARG003
|
||||||
return cast("str", rich_escape(text))
|
return Text()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def format_args(cls, args: dict[str, Any], max_length: int = 500) -> str:
|
def create_static(cls, content: Text, status: str) -> Static:
|
||||||
if not args:
|
css_classes = cls.get_css_classes(status)
|
||||||
return ""
|
return Static(content, classes=css_classes)
|
||||||
|
|
||||||
args_parts = []
|
|
||||||
for k, v in args.items():
|
|
||||||
str_v = str(v)
|
|
||||||
if len(str_v) > max_length:
|
|
||||||
str_v = str_v[: max_length - 3] + "..."
|
|
||||||
args_parts.append(f" [dim]{k}:[/] {cls.escape_markup(str_v)}")
|
|
||||||
return "\n".join(args_parts)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def format_result(cls, result: Any, max_length: int = 1000) -> str:
|
def truncate(cls, text: str, max_length: int = 500) -> str:
|
||||||
if result is None:
|
if len(text) <= max_length:
|
||||||
return ""
|
return text
|
||||||
|
return text[: max_length - 3] + "..."
|
||||||
str_result = str(result).strip()
|
|
||||||
if not str_result:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
if len(str_result) > max_length:
|
|
||||||
str_result = str_result[: max_length - 3] + "..."
|
|
||||||
return cls.escape_markup(str_result)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_status_icon(cls, status: str) -> str:
|
def status_icon(cls, status: str) -> tuple[str, str]:
|
||||||
status_icons = {
|
icons = {
|
||||||
"running": "[#f59e0b]●[/#f59e0b] In progress...",
|
"running": ("● In progress...", "#f59e0b"),
|
||||||
"completed": "[#22c55e]✓[/#22c55e] Done",
|
"completed": ("✓ Done", "#22c55e"),
|
||||||
"failed": "[#dc2626]✗[/#dc2626] Failed",
|
"failed": ("✗ Failed", "#dc2626"),
|
||||||
"error": "[#dc2626]✗[/#dc2626] Error",
|
"error": ("✗ Error", "#dc2626"),
|
||||||
}
|
}
|
||||||
return status_icons.get(status, "[dim]○[/dim] Unknown")
|
return icons.get(status, ("○ Unknown", "dim"))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_css_classes(cls, status: str) -> str:
|
def get_css_classes(cls, status: str) -> str:
|
||||||
base_classes = cls.css_classes.copy()
|
base_classes = cls.css_classes.copy()
|
||||||
base_classes.append(f"status-{status}")
|
base_classes.append(f"status-{status}")
|
||||||
return " ".join(base_classes)
|
return " ".join(base_classes)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def text_with_style(cls, content: str, style: str | None = None) -> Text:
|
||||||
|
text = Text()
|
||||||
|
text.append(content, style=style)
|
||||||
|
return text
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def text_icon_label(
|
||||||
|
cls,
|
||||||
|
icon: str,
|
||||||
|
label: str,
|
||||||
|
icon_style: str | None = None,
|
||||||
|
label_style: str | None = None,
|
||||||
|
) -> Text:
|
||||||
|
text = Text()
|
||||||
|
text.append(icon, style=icon_style)
|
||||||
|
text.append(" ")
|
||||||
|
text.append(label, style=label_style)
|
||||||
|
return text
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def text_header(
|
||||||
|
cls,
|
||||||
|
icon: str,
|
||||||
|
title: str,
|
||||||
|
subtitle: str = "",
|
||||||
|
title_style: str = "bold",
|
||||||
|
subtitle_style: str = "dim",
|
||||||
|
) -> Text:
|
||||||
|
text = Text()
|
||||||
|
text.append(icon)
|
||||||
|
text.append(" ")
|
||||||
|
text.append(title, style=title_style)
|
||||||
|
if subtitle:
|
||||||
|
text.append(" ")
|
||||||
|
text.append(subtitle, style=subtitle_style)
|
||||||
|
return text
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def text_key_value(
|
||||||
|
cls,
|
||||||
|
key: str,
|
||||||
|
value: str,
|
||||||
|
key_style: str = "dim",
|
||||||
|
value_style: str | None = None,
|
||||||
|
indent: int = 2,
|
||||||
|
) -> Text:
|
||||||
|
text = Text()
|
||||||
|
text.append(" " * indent)
|
||||||
|
text.append(key, style=key_style)
|
||||||
|
text.append(": ")
|
||||||
|
text.append(value, style=value_style)
|
||||||
|
return text
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from typing import Any, ClassVar
|
|||||||
|
|
||||||
from pygments.lexers import get_lexer_by_name
|
from pygments.lexers import get_lexer_by_name
|
||||||
from pygments.styles import get_style_by_name
|
from pygments.styles import get_style_by_name
|
||||||
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
from .base_renderer import BaseToolRenderer
|
from .base_renderer import BaseToolRenderer
|
||||||
@@ -20,104 +21,7 @@ class BrowserRenderer(BaseToolRenderer):
|
|||||||
tool_name: ClassVar[str] = "browser_action"
|
tool_name: ClassVar[str] = "browser_action"
|
||||||
css_classes: ClassVar[list[str]] = ["tool-call", "browser-tool"]
|
css_classes: ClassVar[list[str]] = ["tool-call", "browser-tool"]
|
||||||
|
|
||||||
@classmethod
|
SIMPLE_ACTIONS: ClassVar[dict[str, str]] = {
|
||||||
def _get_token_color(cls, token_type: Any) -> str | None:
|
|
||||||
colors = _get_style_colors()
|
|
||||||
while token_type:
|
|
||||||
if token_type in colors:
|
|
||||||
return colors[token_type]
|
|
||||||
token_type = token_type.parent
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _highlight_js(cls, code: str) -> str:
|
|
||||||
lexer = get_lexer_by_name("javascript")
|
|
||||||
result_parts: list[str] = []
|
|
||||||
|
|
||||||
for token_type, token_value in lexer.get_tokens(code):
|
|
||||||
if not token_value:
|
|
||||||
continue
|
|
||||||
|
|
||||||
escaped_value = cls.escape_markup(token_value)
|
|
||||||
color = cls._get_token_color(token_type)
|
|
||||||
|
|
||||||
if color:
|
|
||||||
result_parts.append(f"[{color}]{escaped_value}[/]")
|
|
||||||
else:
|
|
||||||
result_parts.append(escaped_value)
|
|
||||||
|
|
||||||
return "".join(result_parts)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
||||||
args = tool_data.get("args", {})
|
|
||||||
status = tool_data.get("status", "unknown")
|
|
||||||
|
|
||||||
action = args.get("action", "unknown")
|
|
||||||
|
|
||||||
content = cls._build_sleek_content(action, args)
|
|
||||||
|
|
||||||
css_classes = cls.get_css_classes(status)
|
|
||||||
return Static(content, classes=css_classes)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _build_sleek_content(cls, action: str, args: dict[str, Any]) -> str:
|
|
||||||
browser_icon = "🌐"
|
|
||||||
|
|
||||||
url = args.get("url")
|
|
||||||
text = args.get("text")
|
|
||||||
js_code = args.get("js_code")
|
|
||||||
key = args.get("key")
|
|
||||||
file_path = args.get("file_path")
|
|
||||||
|
|
||||||
if action in [
|
|
||||||
"launch",
|
|
||||||
"goto",
|
|
||||||
"new_tab",
|
|
||||||
"type",
|
|
||||||
"execute_js",
|
|
||||||
"click",
|
|
||||||
"double_click",
|
|
||||||
"hover",
|
|
||||||
"press_key",
|
|
||||||
"save_pdf",
|
|
||||||
]:
|
|
||||||
if action == "launch":
|
|
||||||
display_url = cls._format_url(url) if url else None
|
|
||||||
message = (
|
|
||||||
f"launching {display_url} on browser" if display_url else "launching browser"
|
|
||||||
)
|
|
||||||
elif action == "goto":
|
|
||||||
display_url = cls._format_url(url) if url else None
|
|
||||||
message = f"navigating to {display_url}" if display_url else "navigating"
|
|
||||||
elif action == "new_tab":
|
|
||||||
display_url = cls._format_url(url) if url else None
|
|
||||||
message = f"opening tab {display_url}" if display_url else "opening tab"
|
|
||||||
elif action == "type":
|
|
||||||
display_text = cls._format_text(text) if text else None
|
|
||||||
message = f"typing {display_text}" if display_text else "typing"
|
|
||||||
elif action == "execute_js":
|
|
||||||
display_js = cls._format_js(js_code) if js_code else None
|
|
||||||
message = (
|
|
||||||
f"executing javascript\n{display_js}" if display_js else "executing javascript"
|
|
||||||
)
|
|
||||||
elif action == "press_key":
|
|
||||||
display_key = cls.escape_markup(key) if key else None
|
|
||||||
message = f"pressing key {display_key}" if display_key else "pressing key"
|
|
||||||
elif action == "save_pdf":
|
|
||||||
display_path = cls.escape_markup(file_path) if file_path else None
|
|
||||||
message = f"saving PDF to {display_path}" if display_path else "saving PDF"
|
|
||||||
else:
|
|
||||||
action_words = {
|
|
||||||
"click": "clicking",
|
|
||||||
"double_click": "double clicking",
|
|
||||||
"hover": "hovering",
|
|
||||||
}
|
|
||||||
message = cls.escape_markup(action_words[action])
|
|
||||||
|
|
||||||
return f"{browser_icon} [#06b6d4]{message}[/]"
|
|
||||||
|
|
||||||
simple_actions = {
|
|
||||||
"back": "going back in browser history",
|
"back": "going back in browser history",
|
||||||
"forward": "going forward in browser history",
|
"forward": "going forward in browser history",
|
||||||
"scroll_down": "scrolling down",
|
"scroll_down": "scrolling down",
|
||||||
@@ -133,24 +37,99 @@ class BrowserRenderer(BaseToolRenderer):
|
|||||||
"close": "closing browser",
|
"close": "closing browser",
|
||||||
}
|
}
|
||||||
|
|
||||||
if action in simple_actions:
|
@classmethod
|
||||||
return f"{browser_icon} [#06b6d4]{cls.escape_markup(simple_actions[action])}[/]"
|
def _get_token_color(cls, token_type: Any) -> str | None:
|
||||||
|
colors = _get_style_colors()
|
||||||
return f"{browser_icon} [#06b6d4]{cls.escape_markup(action)}[/]"
|
while token_type:
|
||||||
|
if token_type in colors:
|
||||||
|
return colors[token_type]
|
||||||
|
token_type = token_type.parent
|
||||||
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _format_url(cls, url: str) -> str:
|
def _highlight_js(cls, code: str) -> Text:
|
||||||
if len(url) > 300:
|
lexer = get_lexer_by_name("javascript")
|
||||||
url = url[:297] + "..."
|
text = Text()
|
||||||
return cls.escape_markup(url)
|
|
||||||
|
for token_type, token_value in lexer.get_tokens(code):
|
||||||
|
if not token_value:
|
||||||
|
continue
|
||||||
|
color = cls._get_token_color(token_type)
|
||||||
|
text.append(token_value, style=color)
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _format_text(cls, text: str) -> str:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
if len(text) > 200:
|
args = tool_data.get("args", {})
|
||||||
text = text[:197] + "..."
|
status = tool_data.get("status", "unknown")
|
||||||
return cls.escape_markup(text)
|
|
||||||
|
action = args.get("action", "unknown")
|
||||||
|
content = cls._build_content(action, args)
|
||||||
|
|
||||||
|
css_classes = cls.get_css_classes(status)
|
||||||
|
return Static(content, classes=css_classes)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _format_js(cls, js_code: str) -> str:
|
def _build_url_action(cls, text: Text, label: str, url: str | None, suffix: str = "") -> None:
|
||||||
code_display = js_code[:2000] + "..." if len(js_code) > 2000 else js_code
|
text.append(label, style="#06b6d4")
|
||||||
return cls._highlight_js(code_display)
|
if url:
|
||||||
|
text.append(cls.truncate(url, 300), style="#06b6d4")
|
||||||
|
if suffix:
|
||||||
|
text.append(suffix, style="#06b6d4")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _build_content(cls, action: str, args: dict[str, Any]) -> Text:
|
||||||
|
text = Text()
|
||||||
|
text.append("🌐 ")
|
||||||
|
|
||||||
|
if action in cls.SIMPLE_ACTIONS:
|
||||||
|
text.append(cls.SIMPLE_ACTIONS[action], style="#06b6d4")
|
||||||
|
return text
|
||||||
|
|
||||||
|
url = args.get("url")
|
||||||
|
|
||||||
|
url_actions = {
|
||||||
|
"launch": ("launching ", " on browser" if url else "browser"),
|
||||||
|
"goto": ("navigating to ", ""),
|
||||||
|
"new_tab": ("opening tab ", ""),
|
||||||
|
}
|
||||||
|
if action in url_actions:
|
||||||
|
label, suffix = url_actions[action]
|
||||||
|
if action == "launch" and not url:
|
||||||
|
text.append("launching browser", style="#06b6d4")
|
||||||
|
else:
|
||||||
|
cls._build_url_action(text, label, url, suffix)
|
||||||
|
return text
|
||||||
|
|
||||||
|
click_actions = {
|
||||||
|
"click": "clicking",
|
||||||
|
"double_click": "double clicking",
|
||||||
|
"hover": "hovering",
|
||||||
|
}
|
||||||
|
if action in click_actions:
|
||||||
|
text.append(click_actions[action], style="#06b6d4")
|
||||||
|
return text
|
||||||
|
|
||||||
|
handlers: dict[str, tuple[str, str | None]] = {
|
||||||
|
"type": ("typing ", args.get("text")),
|
||||||
|
"press_key": ("pressing key ", args.get("key")),
|
||||||
|
"save_pdf": ("saving PDF to ", args.get("file_path")),
|
||||||
|
}
|
||||||
|
if action in handlers:
|
||||||
|
label, value = handlers[action]
|
||||||
|
text.append(label, style="#06b6d4")
|
||||||
|
if value:
|
||||||
|
text.append(cls.truncate(str(value), 200), style="#06b6d4")
|
||||||
|
return text
|
||||||
|
|
||||||
|
if action == "execute_js":
|
||||||
|
text.append("executing javascript", style="#06b6d4")
|
||||||
|
js_code = args.get("js_code")
|
||||||
|
if js_code:
|
||||||
|
text.append("\n")
|
||||||
|
text.append_text(cls._highlight_js(cls.truncate(js_code, 2000)))
|
||||||
|
return text
|
||||||
|
|
||||||
|
text.append(action, style="#06b6d4")
|
||||||
|
return text
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from typing import Any, ClassVar
|
|||||||
from pygments.lexers import get_lexer_by_name, get_lexer_for_filename
|
from pygments.lexers import get_lexer_by_name, get_lexer_for_filename
|
||||||
from pygments.styles import get_style_by_name
|
from pygments.styles import get_style_by_name
|
||||||
from pygments.util import ClassNotFound
|
from pygments.util import ClassNotFound
|
||||||
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
from .base_renderer import BaseToolRenderer
|
from .base_renderer import BaseToolRenderer
|
||||||
@@ -38,23 +39,17 @@ class StrReplaceEditorRenderer(BaseToolRenderer):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _highlight_code(cls, code: str, path: str) -> str:
|
def _highlight_code(cls, code: str, path: str) -> Text:
|
||||||
lexer = _get_lexer_for_file(path)
|
lexer = _get_lexer_for_file(path)
|
||||||
result_parts: list[str] = []
|
text = Text()
|
||||||
|
|
||||||
for token_type, token_value in lexer.get_tokens(code):
|
for token_type, token_value in lexer.get_tokens(code):
|
||||||
if not token_value:
|
if not token_value:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
escaped_value = cls.escape_markup(token_value)
|
|
||||||
color = cls._get_token_color(token_type)
|
color = cls._get_token_color(token_type)
|
||||||
|
text.append(token_value, style=color)
|
||||||
|
|
||||||
if color:
|
return text
|
||||||
result_parts.append(f"[{color}]{escaped_value}[/]")
|
|
||||||
else:
|
|
||||||
result_parts.append(escaped_value)
|
|
||||||
|
|
||||||
return "".join(result_parts)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
@@ -67,48 +62,64 @@ class StrReplaceEditorRenderer(BaseToolRenderer):
|
|||||||
new_str = args.get("new_str", "")
|
new_str = args.get("new_str", "")
|
||||||
file_text = args.get("file_text", "")
|
file_text = args.get("file_text", "")
|
||||||
|
|
||||||
if command == "view":
|
text = Text()
|
||||||
header = "📖 [bold #10b981]Reading file[/]"
|
|
||||||
elif command == "str_replace":
|
|
||||||
header = "✏️ [bold #10b981]Editing file[/]"
|
|
||||||
elif command == "create":
|
|
||||||
header = "📝 [bold #10b981]Creating file[/]"
|
|
||||||
elif command == "insert":
|
|
||||||
header = "✏️ [bold #10b981]Inserting text[/]"
|
|
||||||
elif command == "undo_edit":
|
|
||||||
header = "↩️ [bold #10b981]Undoing edit[/]"
|
|
||||||
else:
|
|
||||||
header = "📄 [bold #10b981]File operation[/]"
|
|
||||||
|
|
||||||
|
icons_and_labels = {
|
||||||
|
"view": ("📖 ", "Reading file", "#10b981"),
|
||||||
|
"str_replace": ("✏️ ", "Editing file", "#10b981"),
|
||||||
|
"create": ("📝 ", "Creating file", "#10b981"),
|
||||||
|
"insert": ("✏️ ", "Inserting text", "#10b981"),
|
||||||
|
"undo_edit": ("↩️ ", "Undoing edit", "#10b981"),
|
||||||
|
}
|
||||||
|
|
||||||
|
icon, label, color = icons_and_labels.get(command, ("📄 ", "File operation", "#10b981"))
|
||||||
|
text.append(icon)
|
||||||
|
text.append(label, style=f"bold {color}")
|
||||||
|
|
||||||
|
if path:
|
||||||
path_display = path[-60:] if len(path) > 60 else path
|
path_display = path[-60:] if len(path) > 60 else path
|
||||||
content_parts = [f"{header} [dim]{cls.escape_markup(path_display)}[/]"]
|
text.append(" ")
|
||||||
|
text.append(path_display, style="dim")
|
||||||
|
|
||||||
if command == "str_replace" and (old_str or new_str):
|
if command == "str_replace" and (old_str or new_str):
|
||||||
if old_str:
|
if old_str:
|
||||||
old_display = old_str[:1000] + "..." if len(old_str) > 1000 else old_str
|
old_display = cls.truncate(old_str, 1000)
|
||||||
highlighted_old = cls._highlight_code(old_display, path)
|
highlighted_old = cls._highlight_code(old_display, path)
|
||||||
old_lines = highlighted_old.split("\n")
|
for line in highlighted_old.plain.split("\n"):
|
||||||
content_parts.extend(f"[#ef4444]-[/] {line}" for line in old_lines)
|
text.append("\n")
|
||||||
if new_str:
|
text.append("-", style="#ef4444")
|
||||||
new_display = new_str[:1000] + "..." if len(new_str) > 1000 else new_str
|
text.append(" ")
|
||||||
highlighted_new = cls._highlight_code(new_display, path)
|
text.append(line)
|
||||||
new_lines = highlighted_new.split("\n")
|
|
||||||
content_parts.extend(f"[#22c55e]+[/] {line}" for line in new_lines)
|
if new_str:
|
||||||
elif command == "create" and file_text:
|
new_display = cls.truncate(new_str, 1000)
|
||||||
text_display = file_text[:1500] + "..." if len(file_text) > 1500 else file_text
|
highlighted_new = cls._highlight_code(new_display, path)
|
||||||
highlighted_text = cls._highlight_code(text_display, path)
|
for line in highlighted_new.plain.split("\n"):
|
||||||
content_parts.append(highlighted_text)
|
text.append("\n")
|
||||||
elif command == "insert" and new_str:
|
text.append("+", style="#22c55e")
|
||||||
new_display = new_str[:1000] + "..." if len(new_str) > 1000 else new_str
|
text.append(" ")
|
||||||
highlighted_new = cls._highlight_code(new_display, path)
|
text.append(line)
|
||||||
new_lines = highlighted_new.split("\n")
|
|
||||||
content_parts.extend(f"[#22c55e]+[/] {line}" for line in new_lines)
|
elif command == "create" and file_text:
|
||||||
elif not (result and isinstance(result, dict) and "content" in result) and not path:
|
text_display = cls.truncate(file_text, 1500)
|
||||||
content_parts = [f"{header} [dim]Processing...[/]"]
|
text.append("\n")
|
||||||
|
text.append_text(cls._highlight_code(text_display, path))
|
||||||
|
|
||||||
|
elif command == "insert" and new_str:
|
||||||
|
new_display = cls.truncate(new_str, 1000)
|
||||||
|
highlighted_new = cls._highlight_code(new_display, path)
|
||||||
|
for line in highlighted_new.plain.split("\n"):
|
||||||
|
text.append("\n")
|
||||||
|
text.append("+", style="#22c55e")
|
||||||
|
text.append(" ")
|
||||||
|
text.append(line)
|
||||||
|
|
||||||
|
elif not (result and isinstance(result, dict) and "content" in result) and not path:
|
||||||
|
text.append(" ")
|
||||||
|
text.append("Processing...", style="dim")
|
||||||
|
|
||||||
content_text = "\n".join(content_parts)
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -119,19 +130,21 @@ class ListFilesRenderer(BaseToolRenderer):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
args = tool_data.get("args", {})
|
args = tool_data.get("args", {})
|
||||||
|
|
||||||
path = args.get("path", "")
|
path = args.get("path", "")
|
||||||
|
|
||||||
header = "📂 [bold #10b981]Listing files[/]"
|
text = Text()
|
||||||
|
text.append("📂 ")
|
||||||
|
text.append("Listing files", style="bold #10b981")
|
||||||
|
text.append(" ")
|
||||||
|
|
||||||
if path:
|
if path:
|
||||||
path_display = path[-60:] if len(path) > 60 else path
|
path_display = path[-60:] if len(path) > 60 else path
|
||||||
content_text = f"{header} [dim]{cls.escape_markup(path_display)}[/]"
|
text.append(path_display, style="dim")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header} [dim]Current directory[/]"
|
text.append("Current directory", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -142,27 +155,31 @@ class SearchFilesRenderer(BaseToolRenderer):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
args = tool_data.get("args", {})
|
args = tool_data.get("args", {})
|
||||||
|
|
||||||
path = args.get("path", "")
|
path = args.get("path", "")
|
||||||
regex = args.get("regex", "")
|
regex = args.get("regex", "")
|
||||||
|
|
||||||
header = "🔍 [bold purple]Searching files[/]"
|
text = Text()
|
||||||
|
text.append("🔍 ")
|
||||||
|
text.append("Searching files", style="bold purple")
|
||||||
|
text.append(" ")
|
||||||
|
|
||||||
if path and regex:
|
if path and regex:
|
||||||
path_display = path[-30:] if len(path) > 30 else path
|
path_display = path[-30:] if len(path) > 30 else path
|
||||||
regex_display = regex[:30] if len(regex) > 30 else regex
|
regex_display = regex[:30] if len(regex) > 30 else regex
|
||||||
content_text = (
|
text.append(path_display, style="dim")
|
||||||
f"{header} [dim]{cls.escape_markup(path_display)} for "
|
text.append(" for '", style="dim")
|
||||||
f"'{cls.escape_markup(regex_display)}'[/]"
|
text.append(regex_display, style="dim")
|
||||||
)
|
text.append("'", style="dim")
|
||||||
elif path:
|
elif path:
|
||||||
path_display = path[-60:] if len(path) > 60 else path
|
path_display = path[-60:] if len(path) > 60 else path
|
||||||
content_text = f"{header} [dim]{cls.escape_markup(path_display)}[/]"
|
text.append(path_display, style="dim")
|
||||||
elif regex:
|
elif regex:
|
||||||
regex_display = regex[:60] if len(regex) > 60 else regex
|
regex_display = regex[:60] if len(regex) > 60 else regex
|
||||||
content_text = f"{header} [dim]'{cls.escape_markup(regex_display)}'[/]"
|
text.append("'", style="dim")
|
||||||
|
text.append(regex_display, style="dim")
|
||||||
|
text.append("'", style="dim")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header} [dim]Searching...[/]"
|
text.append("Searching...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
from .base_renderer import BaseToolRenderer
|
from .base_renderer import BaseToolRenderer
|
||||||
@@ -18,14 +19,20 @@ class FinishScanRenderer(BaseToolRenderer):
|
|||||||
content = args.get("content", "")
|
content = args.get("content", "")
|
||||||
success = args.get("success", True)
|
success = args.get("success", True)
|
||||||
|
|
||||||
header = (
|
text = Text()
|
||||||
"🏁 [bold #dc2626]Finishing Scan[/]" if success else "🏁 [bold #dc2626]Scan Failed[/]"
|
text.append("🏁 ")
|
||||||
)
|
|
||||||
|
if success:
|
||||||
|
text.append("Finishing Scan", style="bold #dc2626")
|
||||||
|
else:
|
||||||
|
text.append("Scan Failed", style="bold #dc2626")
|
||||||
|
|
||||||
|
text.append("\n ")
|
||||||
|
|
||||||
if content:
|
if content:
|
||||||
content_text = f"{header}\n [bold]{cls.escape_markup(content)}[/]"
|
text.append(content, style="bold")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Generating final report...[/]"
|
text.append("Generating final report...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
from .base_renderer import BaseToolRenderer
|
from .base_renderer import BaseToolRenderer
|
||||||
from .registry import register_tool_renderer
|
from .registry import register_tool_renderer
|
||||||
|
|
||||||
|
|
||||||
def _truncate(text: str, length: int = 800) -> str:
|
|
||||||
if len(text) <= length:
|
|
||||||
return text
|
|
||||||
return text[: length - 3] + "..."
|
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
class CreateNoteRenderer(BaseToolRenderer):
|
class CreateNoteRenderer(BaseToolRenderer):
|
||||||
tool_name: ClassVar[str] = "create_note"
|
tool_name: ClassVar[str] = "create_note"
|
||||||
@@ -25,22 +20,26 @@ class CreateNoteRenderer(BaseToolRenderer):
|
|||||||
content = args.get("content", "")
|
content = args.get("content", "")
|
||||||
category = args.get("category", "general")
|
category = args.get("category", "general")
|
||||||
|
|
||||||
header = f"📝 [bold #fbbf24]Note[/] [dim]({category})[/]"
|
text = Text()
|
||||||
|
text.append("📝 ")
|
||||||
|
text.append("Note", style="bold #fbbf24")
|
||||||
|
text.append(" ")
|
||||||
|
text.append(f"({category})", style="dim")
|
||||||
|
|
||||||
lines = [header]
|
|
||||||
if title:
|
if title:
|
||||||
title_display = _truncate(title.strip(), 300)
|
text.append("\n ")
|
||||||
lines.append(f" {cls.escape_markup(title_display)}")
|
text.append(cls.truncate(title.strip(), 300))
|
||||||
|
|
||||||
if content:
|
if content:
|
||||||
content_display = _truncate(content.strip(), 800)
|
text.append("\n ")
|
||||||
lines.append(f" [dim]{cls.escape_markup(content_display)}[/]")
|
text.append(cls.truncate(content.strip(), 800), style="dim")
|
||||||
|
|
||||||
if len(lines) == 1:
|
if not title and not content:
|
||||||
lines.append(" [dim]Capturing...[/]")
|
text.append("\n ")
|
||||||
|
text.append("Capturing...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static("\n".join(lines), classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -50,11 +49,12 @@ class DeleteNoteRenderer(BaseToolRenderer):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
|
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
|
||||||
header = "📝 [bold #94a3b8]Note Removed[/]"
|
text = Text()
|
||||||
content_text = header
|
text.append("📝 ")
|
||||||
|
text.append("Note Removed", style="bold #94a3b8")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -69,21 +69,24 @@ class UpdateNoteRenderer(BaseToolRenderer):
|
|||||||
title = args.get("title")
|
title = args.get("title")
|
||||||
content = args.get("content")
|
content = args.get("content")
|
||||||
|
|
||||||
header = "📝 [bold #fbbf24]Note Updated[/]"
|
text = Text()
|
||||||
lines = [header]
|
text.append("📝 ")
|
||||||
|
text.append("Note Updated", style="bold #fbbf24")
|
||||||
|
|
||||||
if title:
|
if title:
|
||||||
lines.append(f" {cls.escape_markup(_truncate(title, 300))}")
|
text.append("\n ")
|
||||||
|
text.append(cls.truncate(title, 300))
|
||||||
|
|
||||||
if content:
|
if content:
|
||||||
content_display = _truncate(content.strip(), 800)
|
text.append("\n ")
|
||||||
lines.append(f" [dim]{cls.escape_markup(content_display)}[/]")
|
text.append(cls.truncate(content.strip(), 800), style="dim")
|
||||||
|
|
||||||
if len(lines) == 1:
|
if not title and not content:
|
||||||
lines.append(" [dim]Updating...[/]")
|
text.append("\n ")
|
||||||
|
text.append("Updating...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static("\n".join(lines), classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -95,34 +98,38 @@ class ListNotesRenderer(BaseToolRenderer):
|
|||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
result = tool_data.get("result")
|
result = tool_data.get("result")
|
||||||
|
|
||||||
header = "📝 [bold #fbbf24]Notes[/]"
|
text = Text()
|
||||||
|
text.append("📝 ")
|
||||||
|
text.append("Notes", style="bold #fbbf24")
|
||||||
|
|
||||||
if result and isinstance(result, dict) and result.get("success"):
|
if result and isinstance(result, dict) and result.get("success"):
|
||||||
count = result.get("total_count", 0)
|
count = result.get("total_count", 0)
|
||||||
notes = result.get("notes", []) or []
|
notes = result.get("notes", []) or []
|
||||||
lines = [header]
|
|
||||||
|
|
||||||
if count == 0:
|
if count == 0:
|
||||||
lines.append(" [dim]No notes[/]")
|
text.append("\n ")
|
||||||
|
text.append("No notes", style="dim")
|
||||||
else:
|
else:
|
||||||
for note in notes[:5]:
|
for note in notes[:5]:
|
||||||
title = note.get("title", "").strip() or "(untitled)"
|
title = note.get("title", "").strip() or "(untitled)"
|
||||||
category = note.get("category", "general")
|
category = note.get("category", "general")
|
||||||
content = note.get("content", "").strip()
|
note_content = note.get("content", "").strip()
|
||||||
|
|
||||||
lines.append(
|
text.append("\n - ")
|
||||||
f" - {cls.escape_markup(_truncate(title, 300))} [dim]({category})[/]"
|
text.append(cls.truncate(title, 300))
|
||||||
)
|
text.append(f" ({category})", style="dim")
|
||||||
if content:
|
|
||||||
content_preview = _truncate(content, 400)
|
if note_content:
|
||||||
lines.append(f" [dim]{cls.escape_markup(content_preview)}[/]")
|
text.append("\n ")
|
||||||
|
text.append(cls.truncate(note_content, 400), style="dim")
|
||||||
|
|
||||||
remaining = max(count - 5, 0)
|
remaining = max(count - 5, 0)
|
||||||
if remaining:
|
if remaining:
|
||||||
lines.append(f" [dim]... +{remaining} more[/]")
|
text.append("\n ")
|
||||||
content_text = "\n".join(lines)
|
text.append(f"... +{remaining} more", style="dim")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Loading...[/]"
|
text.append("\n ")
|
||||||
|
text.append("Loading...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
from .base_renderer import BaseToolRenderer
|
from .base_renderer import BaseToolRenderer
|
||||||
@@ -18,38 +19,37 @@ class ListRequestsRenderer(BaseToolRenderer):
|
|||||||
|
|
||||||
httpql_filter = args.get("httpql_filter")
|
httpql_filter = args.get("httpql_filter")
|
||||||
|
|
||||||
header = "📋 [bold #06b6d4]Listing requests[/]"
|
text = Text()
|
||||||
|
text.append("📋 ")
|
||||||
|
text.append("Listing requests", style="bold #06b6d4")
|
||||||
|
|
||||||
if result and isinstance(result, dict) and "requests" in result:
|
if result and isinstance(result, dict) and "requests" in result:
|
||||||
requests = result["requests"]
|
requests = result["requests"]
|
||||||
if isinstance(requests, list) and requests:
|
if isinstance(requests, list) and requests:
|
||||||
request_lines = []
|
|
||||||
for req in requests[:3]:
|
for req in requests[:3]:
|
||||||
if isinstance(req, dict):
|
if isinstance(req, dict):
|
||||||
method = req.get("method", "?")
|
method = req.get("method", "?")
|
||||||
path = req.get("path", "?")
|
path = req.get("path", "?")
|
||||||
response = req.get("response") or {}
|
response = req.get("response") or {}
|
||||||
status = response.get("statusCode", "?")
|
status = response.get("statusCode", "?")
|
||||||
line = f"{method} {path} → {status}"
|
text.append("\n ")
|
||||||
request_lines.append(line)
|
text.append(f"{method} {path} → {status}", style="dim")
|
||||||
|
|
||||||
if len(requests) > 3:
|
if len(requests) > 3:
|
||||||
request_lines.append(f"... +{len(requests) - 3} more")
|
text.append("\n ")
|
||||||
|
text.append(f"... +{len(requests) - 3} more", style="dim")
|
||||||
escaped_lines = [cls.escape_markup(line) for line in request_lines]
|
|
||||||
content_text = f"{header}\n [dim]{chr(10).join(escaped_lines)}[/]"
|
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]No requests found[/]"
|
text.append("\n ")
|
||||||
|
text.append("No requests found", style="dim")
|
||||||
elif httpql_filter:
|
elif httpql_filter:
|
||||||
filter_display = (
|
text.append("\n ")
|
||||||
httpql_filter[:300] + "..." if len(httpql_filter) > 300 else httpql_filter
|
text.append(cls.truncate(httpql_filter, 300), style="dim")
|
||||||
)
|
|
||||||
content_text = f"{header}\n [dim]{cls.escape_markup(filter_display)}[/]"
|
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]All requests[/]"
|
text.append("\n ")
|
||||||
|
text.append("All requests", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -64,34 +64,37 @@ class ViewRequestRenderer(BaseToolRenderer):
|
|||||||
|
|
||||||
part = args.get("part", "request")
|
part = args.get("part", "request")
|
||||||
|
|
||||||
header = f"👀 [bold #06b6d4]Viewing {cls.escape_markup(part)}[/]"
|
text = Text()
|
||||||
|
text.append("👀 ")
|
||||||
|
text.append(f"Viewing {part}", style="bold #06b6d4")
|
||||||
|
|
||||||
if result and isinstance(result, dict):
|
if result and isinstance(result, dict):
|
||||||
if "content" in result:
|
if "content" in result:
|
||||||
content = result["content"]
|
content = result["content"]
|
||||||
content_preview = content[:500] + "..." if len(content) > 500 else content
|
text.append("\n ")
|
||||||
content_text = f"{header}\n [dim]{cls.escape_markup(content_preview)}[/]"
|
text.append(cls.truncate(content, 500), style="dim")
|
||||||
elif "matches" in result:
|
elif "matches" in result:
|
||||||
matches = result["matches"]
|
matches = result["matches"]
|
||||||
if isinstance(matches, list) and matches:
|
if isinstance(matches, list) and matches:
|
||||||
match_lines = [
|
for match in matches[:3]:
|
||||||
match["match"]
|
if isinstance(match, dict) and "match" in match:
|
||||||
for match in matches[:3]
|
text.append("\n ")
|
||||||
if isinstance(match, dict) and "match" in match
|
text.append(match["match"], style="dim")
|
||||||
]
|
|
||||||
if len(matches) > 3:
|
if len(matches) > 3:
|
||||||
match_lines.append(f"... +{len(matches) - 3} more matches")
|
text.append("\n ")
|
||||||
escaped_lines = [cls.escape_markup(line) for line in match_lines]
|
text.append(f"... +{len(matches) - 3} more matches", style="dim")
|
||||||
content_text = f"{header}\n [dim]{chr(10).join(escaped_lines)}[/]"
|
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]No matches found[/]"
|
text.append("\n ")
|
||||||
|
text.append("No matches found", style="dim")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Viewing content...[/]"
|
text.append("\n ")
|
||||||
|
text.append("Viewing content...", style="dim")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Loading...[/]"
|
text.append("\n ")
|
||||||
|
text.append("Loading...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -107,30 +110,32 @@ 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 {cls.escape_markup(method)}[/]"
|
text = Text()
|
||||||
|
text.append("📤 ")
|
||||||
|
text.append(f"Sending {method}", style="bold #06b6d4")
|
||||||
|
|
||||||
if result and isinstance(result, dict):
|
if result and isinstance(result, dict):
|
||||||
status_code = result.get("status_code")
|
status_code = result.get("status_code")
|
||||||
response_body = result.get("body", "")
|
response_body = result.get("body", "")
|
||||||
|
|
||||||
if status_code:
|
if status_code:
|
||||||
response_preview = f"Status: {status_code}"
|
text.append("\n ")
|
||||||
|
text.append(f"Status: {status_code}", style="dim")
|
||||||
if response_body:
|
if response_body:
|
||||||
body_preview = (
|
text.append("\n ")
|
||||||
response_body[:300] + "..." if len(response_body) > 300 else response_body
|
text.append(cls.truncate(response_body, 300), style="dim")
|
||||||
)
|
|
||||||
response_preview += f"\n{body_preview}"
|
|
||||||
content_text = f"{header}\n [dim]{cls.escape_markup(response_preview)}[/]"
|
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Response received[/]"
|
text.append("\n ")
|
||||||
|
text.append("Response received", style="dim")
|
||||||
elif url:
|
elif url:
|
||||||
url_display = url[:400] + "..." if len(url) > 400 else url
|
text.append("\n ")
|
||||||
content_text = f"{header}\n [dim]{cls.escape_markup(url_display)}[/]"
|
text.append(cls.truncate(url, 400), style="dim")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Sending...[/]"
|
text.append("\n ")
|
||||||
|
text.append("Sending...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -145,31 +150,32 @@ class RepeatRequestRenderer(BaseToolRenderer):
|
|||||||
|
|
||||||
modifications = args.get("modifications", {})
|
modifications = args.get("modifications", {})
|
||||||
|
|
||||||
header = "🔄 [bold #06b6d4]Repeating request[/]"
|
text = Text()
|
||||||
|
text.append("🔄 ")
|
||||||
|
text.append("Repeating request", style="bold #06b6d4")
|
||||||
|
|
||||||
if result and isinstance(result, dict):
|
if result and isinstance(result, dict):
|
||||||
status_code = result.get("status_code")
|
status_code = result.get("status_code")
|
||||||
response_body = result.get("body", "")
|
response_body = result.get("body", "")
|
||||||
|
|
||||||
if status_code:
|
if status_code:
|
||||||
response_preview = f"Status: {status_code}"
|
text.append("\n ")
|
||||||
|
text.append(f"Status: {status_code}", style="dim")
|
||||||
if response_body:
|
if response_body:
|
||||||
body_preview = (
|
text.append("\n ")
|
||||||
response_body[:300] + "..." if len(response_body) > 300 else response_body
|
text.append(cls.truncate(response_body, 300), style="dim")
|
||||||
)
|
|
||||||
response_preview += f"\n{body_preview}"
|
|
||||||
content_text = f"{header}\n [dim]{cls.escape_markup(response_preview)}[/]"
|
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Response received[/]"
|
text.append("\n ")
|
||||||
|
text.append("Response received", style="dim")
|
||||||
elif modifications:
|
elif modifications:
|
||||||
mod_text = str(modifications)
|
text.append("\n ")
|
||||||
mod_display = mod_text[:400] + "..." if len(mod_text) > 400 else mod_text
|
text.append(cls.truncate(str(modifications), 400), style="dim")
|
||||||
content_text = f"{header}\n [dim]{cls.escape_markup(mod_display)}[/]"
|
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]No modifications[/]"
|
text.append("\n ")
|
||||||
|
text.append("No modifications", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -179,11 +185,14 @@ class ScopeRulesRenderer(BaseToolRenderer):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
|
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
|
||||||
header = "⚙️ [bold #06b6d4]Updating proxy scope[/]"
|
text = Text()
|
||||||
content_text = f"{header}\n [dim]Configuring...[/]"
|
text.append("⚙️ ")
|
||||||
|
text.append("Updating proxy scope", style="bold #06b6d4")
|
||||||
|
text.append("\n ")
|
||||||
|
text.append("Configuring...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -195,31 +204,32 @@ class ListSitemapRenderer(BaseToolRenderer):
|
|||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
result = tool_data.get("result")
|
result = tool_data.get("result")
|
||||||
|
|
||||||
header = "🗺️ [bold #06b6d4]Listing sitemap[/]"
|
text = Text()
|
||||||
|
text.append("🗺️ ")
|
||||||
|
text.append("Listing sitemap", style="bold #06b6d4")
|
||||||
|
|
||||||
if result and isinstance(result, dict) and "entries" in result:
|
if result and isinstance(result, dict) and "entries" in result:
|
||||||
entries = result["entries"]
|
entries = result["entries"]
|
||||||
if isinstance(entries, list) and entries:
|
if isinstance(entries, list) and entries:
|
||||||
entry_lines = []
|
|
||||||
for entry in entries[:4]:
|
for entry in entries[:4]:
|
||||||
if isinstance(entry, dict):
|
if isinstance(entry, dict):
|
||||||
label = entry.get("label", "?")
|
label = entry.get("label", "?")
|
||||||
kind = entry.get("kind", "?")
|
kind = entry.get("kind", "?")
|
||||||
line = f"{kind}: {label}"
|
text.append("\n ")
|
||||||
entry_lines.append(line)
|
text.append(f"{kind}: {label}", style="dim")
|
||||||
|
|
||||||
if len(entries) > 4:
|
if len(entries) > 4:
|
||||||
entry_lines.append(f"... +{len(entries) - 4} more")
|
text.append("\n ")
|
||||||
|
text.append(f"... +{len(entries) - 4} more", style="dim")
|
||||||
escaped_lines = [cls.escape_markup(line) for line in entry_lines]
|
|
||||||
content_text = f"{header}\n [dim]{chr(10).join(escaped_lines)}[/]"
|
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]No entries found[/]"
|
text.append("\n ")
|
||||||
|
text.append("No entries found", style="dim")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Loading...[/]"
|
text.append("\n ")
|
||||||
|
text.append("Loading...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -231,25 +241,27 @@ class ViewSitemapEntryRenderer(BaseToolRenderer):
|
|||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
result = tool_data.get("result")
|
result = tool_data.get("result")
|
||||||
|
|
||||||
header = "📍 [bold #06b6d4]Viewing sitemap entry[/]"
|
text = Text()
|
||||||
|
text.append("📍 ")
|
||||||
|
text.append("Viewing sitemap entry", style="bold #06b6d4")
|
||||||
|
|
||||||
if result and isinstance(result, dict):
|
if result and isinstance(result, dict) and "entry" in result:
|
||||||
if "entry" in result:
|
|
||||||
entry = result["entry"]
|
entry = result["entry"]
|
||||||
if isinstance(entry, dict):
|
if isinstance(entry, dict):
|
||||||
label = entry.get("label", "")
|
label = entry.get("label", "")
|
||||||
kind = entry.get("kind", "")
|
kind = entry.get("kind", "")
|
||||||
if label and kind:
|
if label and kind:
|
||||||
entry_info = f"{kind}: {label}"
|
text.append("\n ")
|
||||||
content_text = f"{header}\n [dim]{cls.escape_markup(entry_info)}[/]"
|
text.append(f"{kind}: {label}", style="dim")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Entry details loaded[/]"
|
text.append("\n ")
|
||||||
|
text.append("Entry details loaded", style="dim")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Entry details loaded[/]"
|
text.append("\n ")
|
||||||
|
text.append("Entry details loaded", style="dim")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Loading entry...[/]"
|
text.append("\n ")
|
||||||
else:
|
text.append("Loading...", style="dim")
|
||||||
content_text = f"{header}\n [dim]Loading...[/]"
|
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from typing import Any, ClassVar
|
|||||||
|
|
||||||
from pygments.lexers import PythonLexer
|
from pygments.lexers import PythonLexer
|
||||||
from pygments.styles import get_style_by_name
|
from pygments.styles import get_style_by_name
|
||||||
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
from .base_renderer import BaseToolRenderer
|
from .base_renderer import BaseToolRenderer
|
||||||
@@ -30,23 +31,17 @@ class PythonRenderer(BaseToolRenderer):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _highlight_python(cls, code: str) -> str:
|
def _highlight_python(cls, code: str) -> Text:
|
||||||
lexer = PythonLexer()
|
lexer = PythonLexer()
|
||||||
result_parts: list[str] = []
|
text = Text()
|
||||||
|
|
||||||
for token_type, token_value in lexer.get_tokens(code):
|
for token_type, token_value in lexer.get_tokens(code):
|
||||||
if not token_value:
|
if not token_value:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
escaped_value = cls.escape_markup(token_value)
|
|
||||||
color = cls._get_token_color(token_type)
|
color = cls._get_token_color(token_type)
|
||||||
|
text.append(token_value, style=color)
|
||||||
|
|
||||||
if color:
|
return text
|
||||||
result_parts.append(f"[{color}]{escaped_value}[/]")
|
|
||||||
else:
|
|
||||||
result_parts.append(escaped_value)
|
|
||||||
|
|
||||||
return "".join(result_parts)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
@@ -55,18 +50,23 @@ class PythonRenderer(BaseToolRenderer):
|
|||||||
action = args.get("action", "")
|
action = args.get("action", "")
|
||||||
code = args.get("code", "")
|
code = args.get("code", "")
|
||||||
|
|
||||||
header = "</> [bold #3b82f6]Python[/]"
|
text = Text()
|
||||||
|
text.append("</> ")
|
||||||
|
text.append("Python", style="bold #3b82f6")
|
||||||
|
text.append("\n")
|
||||||
|
|
||||||
if code and action in ["new_session", "execute"]:
|
if code and action in ["new_session", "execute"]:
|
||||||
code_display = code[:2000] + "..." if len(code) > 2000 else code
|
code_display = cls.truncate(code, 2000)
|
||||||
highlighted_code = cls._highlight_python(code_display)
|
text.append_text(cls._highlight_python(code_display))
|
||||||
content_text = f"{header}\n{highlighted_code}"
|
|
||||||
elif action == "close":
|
elif action == "close":
|
||||||
content_text = f"{header}\n [dim]Closing session...[/]"
|
text.append(" ")
|
||||||
|
text.append("Closing session...", style="dim")
|
||||||
elif action == "list_sessions":
|
elif action == "list_sessions":
|
||||||
content_text = f"{header}\n [dim]Listing sessions...[/]"
|
text.append(" ")
|
||||||
|
text.append("Listing sessions...", style="dim")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Running...[/]"
|
text.append(" ")
|
||||||
|
text.append("Running...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
from .base_renderer import BaseToolRenderer
|
from .base_renderer import BaseToolRenderer
|
||||||
@@ -47,26 +48,36 @@ def render_tool_widget(tool_data: dict[str, Any]) -> Static:
|
|||||||
|
|
||||||
|
|
||||||
def _render_default_tool_widget(tool_data: dict[str, Any]) -> Static:
|
def _render_default_tool_widget(tool_data: dict[str, Any]) -> Static:
|
||||||
tool_name = BaseToolRenderer.escape_markup(tool_data.get("tool_name", "Unknown Tool"))
|
tool_name = tool_data.get("tool_name", "Unknown Tool")
|
||||||
args = tool_data.get("args", {})
|
args = tool_data.get("args", {})
|
||||||
status = tool_data.get("status", "unknown")
|
status = tool_data.get("status", "unknown")
|
||||||
result = tool_data.get("result")
|
result = tool_data.get("result")
|
||||||
|
|
||||||
status_text = BaseToolRenderer.get_status_icon(status)
|
text = Text()
|
||||||
|
|
||||||
header = f"→ Using tool [bold blue]{BaseToolRenderer.escape_markup(tool_name)}[/]"
|
text.append("→ Using tool ", style="dim")
|
||||||
content_parts = [header]
|
text.append(tool_name, style="bold blue")
|
||||||
|
text.append("\n")
|
||||||
|
|
||||||
args_str = BaseToolRenderer.format_args(args)
|
for k, v in list(args.items())[:2]:
|
||||||
if args_str:
|
str_v = str(v)
|
||||||
content_parts.append(args_str)
|
if len(str_v) > 80:
|
||||||
|
str_v = str_v[:77] + "..."
|
||||||
|
text.append(" ")
|
||||||
|
text.append(k, style="dim")
|
||||||
|
text.append(": ")
|
||||||
|
text.append(str_v)
|
||||||
|
text.append("\n")
|
||||||
|
|
||||||
if status in ["completed", "failed", "error"] and result is not None:
|
if status in ["completed", "failed", "error"] and result is not None:
|
||||||
result_str = BaseToolRenderer.format_result(result)
|
result_str = str(result)
|
||||||
if result_str:
|
if len(result_str) > 150:
|
||||||
content_parts.append(f"[bold]Result:[/] {result_str}")
|
result_str = result_str[:147] + "..."
|
||||||
|
text.append("Result: ", style="bold")
|
||||||
|
text.append(result_str)
|
||||||
else:
|
else:
|
||||||
content_parts.append(status_text)
|
icon, color = BaseToolRenderer.status_icon(status)
|
||||||
|
text.append(icon, style=color)
|
||||||
|
|
||||||
css_classes = BaseToolRenderer.get_css_classes(status)
|
css_classes = BaseToolRenderer.get_css_classes(status)
|
||||||
return Static("\n".join(content_parts), classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
from .base_renderer import BaseToolRenderer
|
from .base_renderer import BaseToolRenderer
|
||||||
@@ -11,6 +12,14 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
|
|||||||
tool_name: ClassVar[str] = "create_vulnerability_report"
|
tool_name: ClassVar[str] = "create_vulnerability_report"
|
||||||
css_classes: ClassVar[list[str]] = ["tool-call", "reporting-tool"]
|
css_classes: ClassVar[list[str]] = ["tool-call", "reporting-tool"]
|
||||||
|
|
||||||
|
SEVERITY_COLORS: ClassVar[dict[str, str]] = {
|
||||||
|
"critical": "#dc2626",
|
||||||
|
"high": "#ea580c",
|
||||||
|
"medium": "#d97706",
|
||||||
|
"low": "#65a30d",
|
||||||
|
"info": "#0284c7",
|
||||||
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
args = tool_data.get("args", {})
|
args = tool_data.get("args", {})
|
||||||
@@ -19,35 +28,25 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
|
|||||||
severity = args.get("severity", "")
|
severity = args.get("severity", "")
|
||||||
content = args.get("content", "")
|
content = args.get("content", "")
|
||||||
|
|
||||||
header = "🐞 [bold #ea580c]Vulnerability Report[/]"
|
text = Text()
|
||||||
|
text.append("🐞 ")
|
||||||
|
text.append("Vulnerability Report", style="bold #ea580c")
|
||||||
|
|
||||||
if title:
|
if title:
|
||||||
content_parts = [f"{header}\n [bold]{cls.escape_markup(title)}[/]"]
|
text.append("\n ")
|
||||||
|
text.append(title, style="bold")
|
||||||
|
|
||||||
if severity:
|
if severity:
|
||||||
severity_color = cls._get_severity_color(severity.lower())
|
severity_color = cls.SEVERITY_COLORS.get(severity.lower(), "#6b7280")
|
||||||
content_parts.append(
|
text.append("\n Severity: ")
|
||||||
f" [dim]Severity: [{severity_color}]"
|
text.append(severity.upper(), style=severity_color)
|
||||||
f"{cls.escape_markup(severity.upper())}[/{severity_color}][/]"
|
|
||||||
)
|
|
||||||
|
|
||||||
if content:
|
if content:
|
||||||
content_parts.append(f" [dim]{cls.escape_markup(content)}[/]")
|
text.append("\n ")
|
||||||
|
text.append(content, style="dim")
|
||||||
content_text = "\n".join(content_parts)
|
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Creating report...[/]"
|
text.append("\n ")
|
||||||
|
text.append("Creating report...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _get_severity_color(cls, severity: str) -> str:
|
|
||||||
severity_colors = {
|
|
||||||
"critical": "#dc2626",
|
|
||||||
"high": "#ea580c",
|
|
||||||
"medium": "#d97706",
|
|
||||||
"low": "#65a30d",
|
|
||||||
"info": "#0284c7",
|
|
||||||
}
|
|
||||||
return severity_colors.get(severity, "#6b7280")
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
from .base_renderer import BaseToolRenderer
|
from .base_renderer import BaseToolRenderer
|
||||||
@@ -15,29 +16,28 @@ class ScanStartInfoRenderer(BaseToolRenderer):
|
|||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
args = tool_data.get("args", {})
|
args = tool_data.get("args", {})
|
||||||
status = tool_data.get("status", "unknown")
|
status = tool_data.get("status", "unknown")
|
||||||
|
|
||||||
targets = args.get("targets", [])
|
targets = args.get("targets", [])
|
||||||
|
|
||||||
|
text = Text()
|
||||||
|
text.append("🚀 Starting penetration test")
|
||||||
|
|
||||||
if len(targets) == 1:
|
if len(targets) == 1:
|
||||||
target_display = cls._build_single_target_display(targets[0])
|
text.append(" on ")
|
||||||
content = f"🚀 Starting penetration test on {target_display}"
|
text.append(cls._get_target_display(targets[0]))
|
||||||
elif len(targets) > 1:
|
elif len(targets) > 1:
|
||||||
content = f"🚀 Starting penetration test on {len(targets)} targets"
|
text.append(f" on {len(targets)} targets")
|
||||||
for target_info in targets:
|
for target_info in targets:
|
||||||
target_display = cls._build_single_target_display(target_info)
|
text.append("\n • ")
|
||||||
content += f"\n • {target_display}"
|
text.append(cls._get_target_display(target_info))
|
||||||
else:
|
|
||||||
content = "🚀 Starting penetration test"
|
|
||||||
|
|
||||||
css_classes = cls.get_css_classes(status)
|
css_classes = cls.get_css_classes(status)
|
||||||
return Static(content, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _build_single_target_display(cls, target_info: dict[str, Any]) -> str:
|
def _get_target_display(cls, target_info: dict[str, Any]) -> str:
|
||||||
original = target_info.get("original")
|
original = target_info.get("original")
|
||||||
if original:
|
if original:
|
||||||
return cls.escape_markup(str(original))
|
return str(original)
|
||||||
|
|
||||||
return "unknown target"
|
return "unknown target"
|
||||||
|
|
||||||
|
|
||||||
@@ -51,14 +51,16 @@ class SubagentStartInfoRenderer(BaseToolRenderer):
|
|||||||
args = tool_data.get("args", {})
|
args = tool_data.get("args", {})
|
||||||
status = tool_data.get("status", "unknown")
|
status = tool_data.get("status", "unknown")
|
||||||
|
|
||||||
name = args.get("name", "Unknown Agent")
|
name = str(args.get("name", "Unknown Agent"))
|
||||||
task = args.get("task", "")
|
task = str(args.get("task", ""))
|
||||||
|
|
||||||
|
text = Text()
|
||||||
|
text.append("🤖 Spawned subagent ")
|
||||||
|
text.append(name)
|
||||||
|
|
||||||
name = cls.escape_markup(str(name))
|
|
||||||
content = f"🤖 Spawned subagent {name}"
|
|
||||||
if task:
|
if task:
|
||||||
task = cls.escape_markup(str(task))
|
text.append("\n Task: ")
|
||||||
content += f"\n Task: {task}"
|
text.append(task)
|
||||||
|
|
||||||
css_classes = cls.get_css_classes(status)
|
css_classes = cls.get_css_classes(status)
|
||||||
return Static(content, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from typing import Any, ClassVar
|
|||||||
|
|
||||||
from pygments.lexers import get_lexer_by_name
|
from pygments.lexers import get_lexer_by_name
|
||||||
from pygments.styles import get_style_by_name
|
from pygments.styles import get_style_by_name
|
||||||
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
from .base_renderer import BaseToolRenderer
|
from .base_renderer import BaseToolRenderer
|
||||||
@@ -20,65 +21,7 @@ class TerminalRenderer(BaseToolRenderer):
|
|||||||
tool_name: ClassVar[str] = "terminal_execute"
|
tool_name: ClassVar[str] = "terminal_execute"
|
||||||
css_classes: ClassVar[list[str]] = ["tool-call", "terminal-tool"]
|
css_classes: ClassVar[list[str]] = ["tool-call", "terminal-tool"]
|
||||||
|
|
||||||
@classmethod
|
CONTROL_SEQUENCES: ClassVar[set[str]] = {
|
||||||
def _get_token_color(cls, token_type: Any) -> str | None:
|
|
||||||
colors = _get_style_colors()
|
|
||||||
while token_type:
|
|
||||||
if token_type in colors:
|
|
||||||
return colors[token_type]
|
|
||||||
token_type = token_type.parent
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _highlight_bash(cls, code: str) -> str:
|
|
||||||
lexer = get_lexer_by_name("bash")
|
|
||||||
result_parts: list[str] = []
|
|
||||||
|
|
||||||
for token_type, token_value in lexer.get_tokens(code):
|
|
||||||
if not token_value:
|
|
||||||
continue
|
|
||||||
|
|
||||||
escaped_value = cls.escape_markup(token_value)
|
|
||||||
color = cls._get_token_color(token_type)
|
|
||||||
|
|
||||||
if color:
|
|
||||||
result_parts.append(f"[{color}]{escaped_value}[/]")
|
|
||||||
else:
|
|
||||||
result_parts.append(escaped_value)
|
|
||||||
|
|
||||||
return "".join(result_parts)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
||||||
args = tool_data.get("args", {})
|
|
||||||
status = tool_data.get("status", "unknown")
|
|
||||||
result = tool_data.get("result", {})
|
|
||||||
|
|
||||||
command = args.get("command", "")
|
|
||||||
is_input = args.get("is_input", False)
|
|
||||||
terminal_id = args.get("terminal_id", "default")
|
|
||||||
timeout = args.get("timeout")
|
|
||||||
|
|
||||||
content = cls._build_sleek_content(command, is_input, terminal_id, timeout, result)
|
|
||||||
|
|
||||||
css_classes = cls.get_css_classes(status)
|
|
||||||
return Static(content, classes=css_classes)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _build_sleek_content(
|
|
||||||
cls,
|
|
||||||
command: str,
|
|
||||||
is_input: bool,
|
|
||||||
terminal_id: str, # noqa: ARG003
|
|
||||||
timeout: float | None, # noqa: ARG003
|
|
||||||
result: dict[str, Any], # noqa: ARG003
|
|
||||||
) -> str:
|
|
||||||
terminal_icon = ">_"
|
|
||||||
|
|
||||||
if not command.strip():
|
|
||||||
return f"{terminal_icon} [dim]getting logs...[/]"
|
|
||||||
|
|
||||||
control_sequences = {
|
|
||||||
"C-c",
|
"C-c",
|
||||||
"C-d",
|
"C-d",
|
||||||
"C-z",
|
"C-z",
|
||||||
@@ -106,7 +49,7 @@ class TerminalRenderer(BaseToolRenderer):
|
|||||||
"^t",
|
"^t",
|
||||||
"^y",
|
"^y",
|
||||||
}
|
}
|
||||||
special_keys = {
|
SPECIAL_KEYS: ClassVar[set[str]] = {
|
||||||
"Enter",
|
"Enter",
|
||||||
"Escape",
|
"Escape",
|
||||||
"Space",
|
"Space",
|
||||||
@@ -141,26 +84,76 @@ class TerminalRenderer(BaseToolRenderer):
|
|||||||
"F12",
|
"F12",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_token_color(cls, token_type: Any) -> str | None:
|
||||||
|
colors = _get_style_colors()
|
||||||
|
while token_type:
|
||||||
|
if token_type in colors:
|
||||||
|
return colors[token_type]
|
||||||
|
token_type = token_type.parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _highlight_bash(cls, code: str) -> Text:
|
||||||
|
lexer = get_lexer_by_name("bash")
|
||||||
|
text = Text()
|
||||||
|
|
||||||
|
for token_type, token_value in lexer.get_tokens(code):
|
||||||
|
if not token_value:
|
||||||
|
continue
|
||||||
|
color = cls._get_token_color(token_type)
|
||||||
|
text.append(token_value, style=color)
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
|
args = tool_data.get("args", {})
|
||||||
|
status = tool_data.get("status", "unknown")
|
||||||
|
|
||||||
|
command = args.get("command", "")
|
||||||
|
is_input = args.get("is_input", False)
|
||||||
|
|
||||||
|
content = cls._build_content(command, is_input)
|
||||||
|
|
||||||
|
css_classes = cls.get_css_classes(status)
|
||||||
|
return Static(content, classes=css_classes)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _build_content(cls, command: str, is_input: bool) -> Text:
|
||||||
|
text = Text()
|
||||||
|
terminal_icon = ">_"
|
||||||
|
|
||||||
|
if not command.strip():
|
||||||
|
text.append(terminal_icon)
|
||||||
|
text.append(" ")
|
||||||
|
text.append("getting logs...", style="dim")
|
||||||
|
return text
|
||||||
|
|
||||||
is_special = (
|
is_special = (
|
||||||
command in control_sequences
|
command in cls.CONTROL_SEQUENCES
|
||||||
or command in special_keys
|
or command in cls.SPECIAL_KEYS
|
||||||
or command.startswith(("M-", "S-", "C-S-", "C-M-", "S-M-"))
|
or command.startswith(("M-", "S-", "C-S-", "C-M-", "S-M-"))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
text.append(terminal_icon)
|
||||||
|
text.append(" ")
|
||||||
|
|
||||||
if is_special:
|
if is_special:
|
||||||
return f"{terminal_icon} [#ef4444]{cls.escape_markup(command)}[/]"
|
text.append(command, style="#ef4444")
|
||||||
|
elif is_input:
|
||||||
|
text.append(">>>", style="#3b82f6")
|
||||||
|
text.append(" ")
|
||||||
|
text.append_text(cls._format_command(command))
|
||||||
|
else:
|
||||||
|
text.append("$", style="#22c55e")
|
||||||
|
text.append(" ")
|
||||||
|
text.append_text(cls._format_command(command))
|
||||||
|
|
||||||
if is_input:
|
return text
|
||||||
formatted_command = cls._format_command_display(command)
|
|
||||||
return f"{terminal_icon} [#3b82f6]>>>[/] {formatted_command}"
|
|
||||||
|
|
||||||
formatted_command = cls._format_command_display(command)
|
|
||||||
return f"{terminal_icon} [#22c55e]$[/] {formatted_command}"
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _format_command_display(cls, command: str) -> str:
|
def _format_command(cls, command: str) -> Text:
|
||||||
if not command:
|
if len(command) > 2000:
|
||||||
return ""
|
command = command[:2000] + "..."
|
||||||
|
return cls._highlight_bash(command)
|
||||||
cmd_display = command[:2000] + "..." if len(command) > 2000 else command
|
|
||||||
return cls._highlight_bash(cmd_display)
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
from .base_renderer import BaseToolRenderer
|
from .base_renderer import BaseToolRenderer
|
||||||
@@ -14,16 +15,18 @@ class ThinkRenderer(BaseToolRenderer):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
args = tool_data.get("args", {})
|
args = tool_data.get("args", {})
|
||||||
|
|
||||||
thought = args.get("thought", "")
|
thought = args.get("thought", "")
|
||||||
|
|
||||||
header = "🧠 [bold #a855f7]Thinking[/]"
|
text = Text()
|
||||||
|
text.append("🧠 ")
|
||||||
|
text.append("Thinking", style="bold #a855f7")
|
||||||
|
text.append("\n ")
|
||||||
|
|
||||||
if thought:
|
if thought:
|
||||||
thought_display = thought[:600] + "..." if len(thought) > 600 else thought
|
thought_display = cls.truncate(thought, 600)
|
||||||
content = f"{header}\n [italic dim]{cls.escape_markup(thought_display)}[/]"
|
text.append(thought_display, style="italic dim")
|
||||||
else:
|
else:
|
||||||
content = f"{header}\n [italic dim]Thinking...[/]"
|
text.append("Thinking...", style="italic dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|||||||
@@ -1,57 +1,53 @@
|
|||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
from .base_renderer import BaseToolRenderer
|
from .base_renderer import BaseToolRenderer
|
||||||
from .registry import register_tool_renderer
|
from .registry import register_tool_renderer
|
||||||
|
|
||||||
|
|
||||||
STATUS_MARKERS = {
|
STATUS_MARKERS: dict[str, str] = {
|
||||||
"pending": "[ ]",
|
"pending": "[ ]",
|
||||||
"in_progress": "[~]",
|
"in_progress": "[~]",
|
||||||
"done": "[•]",
|
"done": "[•]",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _truncate(text: str, length: int = 80) -> str:
|
def _format_todo_lines(text: Text, result: dict[str, Any], limit: int = 25) -> None:
|
||||||
if len(text) <= length:
|
|
||||||
return text
|
|
||||||
return text[: length - 3] + "..."
|
|
||||||
|
|
||||||
|
|
||||||
def _format_todo_lines(
|
|
||||||
cls: type[BaseToolRenderer], result: dict[str, Any], limit: int = 25
|
|
||||||
) -> list[str]:
|
|
||||||
todos = result.get("todos")
|
todos = result.get("todos")
|
||||||
if not isinstance(todos, list) or not todos:
|
if not isinstance(todos, list) or not todos:
|
||||||
return [" [dim]No todos[/]"]
|
text.append("\n ")
|
||||||
|
text.append("No todos", style="dim")
|
||||||
|
return
|
||||||
|
|
||||||
lines: list[str] = []
|
|
||||||
total = len(todos)
|
total = len(todos)
|
||||||
|
|
||||||
for index, todo in enumerate(todos):
|
for index, todo in enumerate(todos):
|
||||||
if index >= limit:
|
if index >= limit:
|
||||||
remaining = total - limit
|
remaining = total - limit
|
||||||
if remaining > 0:
|
if remaining > 0:
|
||||||
lines.append(f" [dim]... +{remaining} more[/]")
|
text.append("\n ")
|
||||||
|
text.append(f"... +{remaining} more", style="dim")
|
||||||
break
|
break
|
||||||
|
|
||||||
status = todo.get("status", "pending")
|
status = todo.get("status", "pending")
|
||||||
marker = STATUS_MARKERS.get(status, STATUS_MARKERS["pending"])
|
marker = STATUS_MARKERS.get(status, STATUS_MARKERS["pending"])
|
||||||
|
|
||||||
title = todo.get("title", "").strip() or "(untitled)"
|
title = todo.get("title", "").strip() or "(untitled)"
|
||||||
title = cls.escape_markup(_truncate(title, 90))
|
if len(title) > 90:
|
||||||
|
title = title[:87] + "..."
|
||||||
|
|
||||||
|
text.append("\n ")
|
||||||
|
text.append(marker)
|
||||||
|
text.append(" ")
|
||||||
|
|
||||||
if status == "done":
|
if status == "done":
|
||||||
title_markup = f"[dim strike]{title}[/]"
|
text.append(title, style="dim strike")
|
||||||
elif status == "in_progress":
|
elif status == "in_progress":
|
||||||
title_markup = f"[italic]{title}[/]"
|
text.append(title, style="italic")
|
||||||
else:
|
else:
|
||||||
title_markup = title
|
text.append(title)
|
||||||
|
|
||||||
lines.append(f" {marker} {title_markup}")
|
|
||||||
|
|
||||||
return lines
|
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -62,21 +58,24 @@ class CreateTodoRenderer(BaseToolRenderer):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
result = tool_data.get("result")
|
result = tool_data.get("result")
|
||||||
header = "📋 [bold #a78bfa]Todo[/]"
|
|
||||||
|
text = Text()
|
||||||
|
text.append("📋 ")
|
||||||
|
text.append("Todo", style="bold #a78bfa")
|
||||||
|
|
||||||
if result and isinstance(result, dict):
|
if result and isinstance(result, dict):
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
lines = [header]
|
_format_todo_lines(text, result)
|
||||||
lines.extend(_format_todo_lines(cls, result))
|
|
||||||
content_text = "\n".join(lines)
|
|
||||||
else:
|
else:
|
||||||
error = result.get("error", "Failed to create todo")
|
error = result.get("error", "Failed to create todo")
|
||||||
content_text = f"{header}\n [#ef4444]{cls.escape_markup(error)}[/]"
|
text.append("\n ")
|
||||||
|
text.append(error, style="#ef4444")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Creating...[/]"
|
text.append("\n ")
|
||||||
|
text.append("Creating...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -87,21 +86,24 @@ class ListTodosRenderer(BaseToolRenderer):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
result = tool_data.get("result")
|
result = tool_data.get("result")
|
||||||
header = "📋 [bold #a78bfa]Todos[/]"
|
|
||||||
|
text = Text()
|
||||||
|
text.append("📋 ")
|
||||||
|
text.append("Todos", style="bold #a78bfa")
|
||||||
|
|
||||||
if result and isinstance(result, dict):
|
if result and isinstance(result, dict):
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
lines = [header]
|
_format_todo_lines(text, result)
|
||||||
lines.extend(_format_todo_lines(cls, result))
|
|
||||||
content_text = "\n".join(lines)
|
|
||||||
else:
|
else:
|
||||||
error = result.get("error", "Unable to list todos")
|
error = result.get("error", "Unable to list todos")
|
||||||
content_text = f"{header}\n [#ef4444]{cls.escape_markup(error)}[/]"
|
text.append("\n ")
|
||||||
|
text.append(error, style="#ef4444")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Loading...[/]"
|
text.append("\n ")
|
||||||
|
text.append("Loading...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -112,21 +114,24 @@ class UpdateTodoRenderer(BaseToolRenderer):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
result = tool_data.get("result")
|
result = tool_data.get("result")
|
||||||
header = "📋 [bold #a78bfa]Todo Updated[/]"
|
|
||||||
|
text = Text()
|
||||||
|
text.append("📋 ")
|
||||||
|
text.append("Todo Updated", style="bold #a78bfa")
|
||||||
|
|
||||||
if result and isinstance(result, dict):
|
if result and isinstance(result, dict):
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
lines = [header]
|
_format_todo_lines(text, result)
|
||||||
lines.extend(_format_todo_lines(cls, result))
|
|
||||||
content_text = "\n".join(lines)
|
|
||||||
else:
|
else:
|
||||||
error = result.get("error", "Failed to update todo")
|
error = result.get("error", "Failed to update todo")
|
||||||
content_text = f"{header}\n [#ef4444]{cls.escape_markup(error)}[/]"
|
text.append("\n ")
|
||||||
|
text.append(error, style="#ef4444")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Updating...[/]"
|
text.append("\n ")
|
||||||
|
text.append("Updating...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -137,21 +142,24 @@ class MarkTodoDoneRenderer(BaseToolRenderer):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
result = tool_data.get("result")
|
result = tool_data.get("result")
|
||||||
header = "📋 [bold #a78bfa]Todo Completed[/]"
|
|
||||||
|
text = Text()
|
||||||
|
text.append("📋 ")
|
||||||
|
text.append("Todo Completed", style="bold #a78bfa")
|
||||||
|
|
||||||
if result and isinstance(result, dict):
|
if result and isinstance(result, dict):
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
lines = [header]
|
_format_todo_lines(text, result)
|
||||||
lines.extend(_format_todo_lines(cls, result))
|
|
||||||
content_text = "\n".join(lines)
|
|
||||||
else:
|
else:
|
||||||
error = result.get("error", "Failed to mark todo done")
|
error = result.get("error", "Failed to mark todo done")
|
||||||
content_text = f"{header}\n [#ef4444]{cls.escape_markup(error)}[/]"
|
text.append("\n ")
|
||||||
|
text.append(error, style="#ef4444")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Marking done...[/]"
|
text.append("\n ")
|
||||||
|
text.append("Marking done...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -162,21 +170,24 @@ class MarkTodoPendingRenderer(BaseToolRenderer):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
result = tool_data.get("result")
|
result = tool_data.get("result")
|
||||||
header = "📋 [bold #f59e0b]Todo Reopened[/]"
|
|
||||||
|
text = Text()
|
||||||
|
text.append("📋 ")
|
||||||
|
text.append("Todo Reopened", style="bold #f59e0b")
|
||||||
|
|
||||||
if result and isinstance(result, dict):
|
if result and isinstance(result, dict):
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
lines = [header]
|
_format_todo_lines(text, result)
|
||||||
lines.extend(_format_todo_lines(cls, result))
|
|
||||||
content_text = "\n".join(lines)
|
|
||||||
else:
|
else:
|
||||||
error = result.get("error", "Failed to reopen todo")
|
error = result.get("error", "Failed to reopen todo")
|
||||||
content_text = f"{header}\n [#ef4444]{cls.escape_markup(error)}[/]"
|
text.append("\n ")
|
||||||
|
text.append(error, style="#ef4444")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Reopening...[/]"
|
text.append("\n ")
|
||||||
|
text.append("Reopening...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -187,18 +198,21 @@ class DeleteTodoRenderer(BaseToolRenderer):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
result = tool_data.get("result")
|
result = tool_data.get("result")
|
||||||
header = "📋 [bold #94a3b8]Todo Removed[/]"
|
|
||||||
|
text = Text()
|
||||||
|
text.append("📋 ")
|
||||||
|
text.append("Todo Removed", style="bold #94a3b8")
|
||||||
|
|
||||||
if result and isinstance(result, dict):
|
if result and isinstance(result, dict):
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
lines = [header]
|
_format_todo_lines(text, result)
|
||||||
lines.extend(_format_todo_lines(cls, result))
|
|
||||||
content_text = "\n".join(lines)
|
|
||||||
else:
|
else:
|
||||||
error = result.get("error", "Failed to remove todo")
|
error = result.get("error", "Failed to remove todo")
|
||||||
content_text = f"{header}\n [#ef4444]{cls.escape_markup(error)}[/]"
|
text.append("\n ")
|
||||||
|
text.append(error, style="#ef4444")
|
||||||
else:
|
else:
|
||||||
content_text = f"{header}\n [dim]Removing...[/]"
|
text.append("\n ")
|
||||||
|
text.append("Removing...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
from .base_renderer import BaseToolRenderer
|
from .base_renderer import BaseToolRenderer
|
||||||
@@ -12,32 +13,41 @@ class UserMessageRenderer(BaseToolRenderer):
|
|||||||
css_classes: ClassVar[list[str]] = ["chat-message", "user-message"]
|
css_classes: ClassVar[list[str]] = ["chat-message", "user-message"]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, message_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
content = message_data.get("content", "")
|
content = tool_data.get("content", "")
|
||||||
|
|
||||||
if not content:
|
if not content:
|
||||||
return Static("", classes=cls.css_classes)
|
return Static(Text(), classes=" ".join(cls.css_classes))
|
||||||
|
|
||||||
if len(content) > 300:
|
styled_text = cls._format_user_message(content)
|
||||||
content = content[:297] + "..."
|
|
||||||
|
|
||||||
lines = content.split("\n")
|
return Static(styled_text, classes=" ".join(cls.css_classes))
|
||||||
bordered_lines = [f"[#3b82f6]▍[/#3b82f6] {line}" for line in lines]
|
|
||||||
bordered_content = "\n".join(bordered_lines)
|
|
||||||
formatted_content = f"[#3b82f6]▍[/#3b82f6] [bold]You:[/]\n{bordered_content}"
|
|
||||||
|
|
||||||
css_classes = " ".join(cls.css_classes)
|
|
||||||
return Static(formatted_content, classes=css_classes)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render_simple(cls, content: str) -> str:
|
def render_simple(cls, content: str) -> Text:
|
||||||
if not content:
|
if not content:
|
||||||
return ""
|
return Text()
|
||||||
|
|
||||||
|
return cls._format_user_message(content)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _format_user_message(cls, content: str) -> Text:
|
||||||
|
text = Text()
|
||||||
|
|
||||||
if len(content) > 300:
|
if len(content) > 300:
|
||||||
content = content[:297] + "..."
|
content = content[:297] + "..."
|
||||||
|
|
||||||
|
text.append("▍", style="#3b82f6")
|
||||||
|
text.append(" ")
|
||||||
|
text.append("You:", style="bold")
|
||||||
|
text.append("\n")
|
||||||
|
|
||||||
lines = content.split("\n")
|
lines = content.split("\n")
|
||||||
bordered_lines = [f"[#3b82f6]▍[/#3b82f6] {line}" for line in lines]
|
for i, line in enumerate(lines):
|
||||||
bordered_content = "\n".join(bordered_lines)
|
if i > 0:
|
||||||
return f"[#3b82f6]▍[/#3b82f6] [bold]You:[/]\n{bordered_content}"
|
text.append("\n")
|
||||||
|
text.append("▍", style="#3b82f6")
|
||||||
|
text.append(" ")
|
||||||
|
text.append(line)
|
||||||
|
|
||||||
|
return text
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
from .base_renderer import BaseToolRenderer
|
from .base_renderer import BaseToolRenderer
|
||||||
@@ -16,13 +17,13 @@ class WebSearchRenderer(BaseToolRenderer):
|
|||||||
args = tool_data.get("args", {})
|
args = tool_data.get("args", {})
|
||||||
query = args.get("query", "")
|
query = args.get("query", "")
|
||||||
|
|
||||||
header = "🌐 [bold #60a5fa]Searching the web...[/]"
|
text = Text()
|
||||||
|
text.append("🌐 ")
|
||||||
|
text.append("Searching the web...", style="bold #60a5fa")
|
||||||
|
|
||||||
if query:
|
if query:
|
||||||
query_display = query[:100] + "..." if len(query) > 100 else query
|
text.append("\n ")
|
||||||
content_text = f"{header}\n [dim]{cls.escape_markup(query_display)}[/]"
|
text.append(cls.truncate(query, 100), style="dim")
|
||||||
else:
|
|
||||||
content_text = f"{header}"
|
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(content_text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import threading
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from importlib.metadata import PackageNotFoundError
|
from importlib.metadata import PackageNotFoundError
|
||||||
from importlib.metadata import version as pkg_version
|
from importlib.metadata import version as pkg_version
|
||||||
from typing import TYPE_CHECKING, Any, ClassVar, cast
|
from typing import TYPE_CHECKING, Any, ClassVar
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -17,7 +17,6 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
from rich.align import Align
|
from rich.align import Align
|
||||||
from rich.console import Group
|
from rich.console import Group
|
||||||
from rich.markup import escape as rich_escape
|
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
@@ -36,10 +35,6 @@ from strix.llm.config import LLMConfig
|
|||||||
from strix.telemetry.tracer import Tracer, set_global_tracer
|
from strix.telemetry.tracer import Tracer, set_global_tracer
|
||||||
|
|
||||||
|
|
||||||
def escape_markup(text: str) -> str:
|
|
||||||
return cast("str", rich_escape(text))
|
|
||||||
|
|
||||||
|
|
||||||
def get_package_version() -> str:
|
def get_package_version() -> str:
|
||||||
try:
|
try:
|
||||||
return pkg_version("strix-agent")
|
return pkg_version("strix-agent")
|
||||||
@@ -259,7 +254,7 @@ class StopAgentScreen(ModalScreen): # type: ignore[misc]
|
|||||||
class QuitScreen(ModalScreen): # type: ignore[misc]
|
class QuitScreen(ModalScreen): # type: ignore[misc]
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Grid(
|
yield Grid(
|
||||||
Label("🦉 Quit Strix? ", id="quit_title"),
|
Label("Quit Strix?", id="quit_title"),
|
||||||
Grid(
|
Grid(
|
||||||
Button("Yes", variant="error", id="quit"),
|
Button("Yes", variant="error", id="quit"),
|
||||||
Button("No", variant="default", id="cancel"),
|
Button("No", variant="default", id="cancel"),
|
||||||
@@ -555,7 +550,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
}
|
}
|
||||||
|
|
||||||
status_icon = status_indicators.get(status, "🔵")
|
status_icon = status_indicators.get(status, "🔵")
|
||||||
agent_name = f"{status_icon} {escape_markup(agent_name_raw)}"
|
agent_name = f"{status_icon} {agent_name_raw}"
|
||||||
|
|
||||||
if status == "running":
|
if status == "running":
|
||||||
self._start_agent_verb_timer(agent_id)
|
self._start_agent_verb_timer(agent_id)
|
||||||
@@ -614,8 +609,7 @@ class StrixTUIApp(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)
|
||||||
self._update_static_content_safe(chat_display, content)
|
self._safe_widget_operation(chat_display.update, content)
|
||||||
|
|
||||||
chat_display.set_classes(css_class)
|
chat_display.set_classes(css_class)
|
||||||
|
|
||||||
if is_at_bottom:
|
if is_at_bottom:
|
||||||
@@ -623,54 +617,70 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
|
|
||||||
def _get_chat_placeholder_content(
|
def _get_chat_placeholder_content(
|
||||||
self, message: str, placeholder_class: str
|
self, message: str, placeholder_class: str
|
||||||
) -> tuple[str, str]:
|
) -> tuple[Text, str]:
|
||||||
self._displayed_events = [placeholder_class]
|
self._displayed_events = [placeholder_class]
|
||||||
return message, f"chat-placeholder {placeholder_class}"
|
text = Text()
|
||||||
|
text.append(message)
|
||||||
|
return text, f"chat-placeholder {placeholder_class}"
|
||||||
|
|
||||||
|
def _get_rendered_events_content(self, events: list[dict[str, Any]]) -> Text:
|
||||||
|
result = Text()
|
||||||
|
|
||||||
def _get_rendered_events_content(self, events: list[dict[str, Any]]) -> str:
|
|
||||||
if not events:
|
if not events:
|
||||||
return ""
|
return result
|
||||||
|
|
||||||
content_lines = []
|
first = True
|
||||||
for event in events:
|
for event in events:
|
||||||
if event["type"] == "chat":
|
content: Text | None = None
|
||||||
chat_content = self._render_chat_content(event["data"])
|
|
||||||
if chat_content:
|
|
||||||
content_lines.append(chat_content)
|
|
||||||
elif event["type"] == "tool":
|
|
||||||
tool_content = self._render_tool_content_simple(event["data"])
|
|
||||||
if tool_content:
|
|
||||||
content_lines.append(tool_content)
|
|
||||||
|
|
||||||
return "\n\n".join(content_lines)
|
if event["type"] == "chat":
|
||||||
|
content = self._render_chat_content(event["data"])
|
||||||
|
elif event["type"] == "tool":
|
||||||
|
content = self._render_tool_content_simple(event["data"])
|
||||||
|
|
||||||
|
if content:
|
||||||
|
if not first:
|
||||||
|
result.append("\n\n")
|
||||||
|
result.append_text(content)
|
||||||
|
first = False
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def _get_status_display_content(
|
def _get_status_display_content(
|
||||||
self, agent_id: str, agent_data: dict[str, Any]
|
self, agent_id: str, agent_data: dict[str, Any]
|
||||||
) -> tuple[str | Text, str, bool]:
|
) -> tuple[Text | None, Text, bool]:
|
||||||
status = agent_data.get("status", "running")
|
status = agent_data.get("status", "running")
|
||||||
|
|
||||||
simple_statuses = {
|
def keymap_text(msg: str) -> Text:
|
||||||
"stopping": ("Agent stopping...", "", False),
|
t = Text()
|
||||||
"stopped": ("Agent stopped", "", False),
|
t.append(msg, style="dim")
|
||||||
"completed": ("Agent completed", "", False),
|
return t
|
||||||
|
|
||||||
|
simple_statuses: dict[str, tuple[str, str]] = {
|
||||||
|
"stopping": ("Agent stopping...", ""),
|
||||||
|
"stopped": ("Agent stopped", ""),
|
||||||
|
"completed": ("Agent completed", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
if status in simple_statuses:
|
if status in simple_statuses:
|
||||||
return simple_statuses[status]
|
msg, km = simple_statuses[status]
|
||||||
|
text = Text()
|
||||||
|
text.append(msg)
|
||||||
|
return (text, keymap_text(km), False)
|
||||||
|
|
||||||
if status == "llm_failed":
|
if status == "llm_failed":
|
||||||
error_msg = agent_data.get("error_message", "")
|
error_msg = agent_data.get("error_message", "")
|
||||||
display_msg = (
|
text = Text()
|
||||||
f"[red]{escape_markup(error_msg)}[/red]"
|
if error_msg:
|
||||||
if error_msg
|
text.append(error_msg, style="red")
|
||||||
else "[red]LLM request failed[/red]"
|
else:
|
||||||
)
|
text.append("LLM request failed", style="red")
|
||||||
self._stop_dot_animation()
|
self._stop_dot_animation()
|
||||||
return (display_msg, "[dim]Send message to retry[/dim]", False)
|
return (text, keymap_text("Send message to retry"), False)
|
||||||
|
|
||||||
if status == "waiting":
|
if status == "waiting":
|
||||||
animated_text = self._get_animated_waiting_text(agent_id)
|
animated_text = self._get_animated_waiting_text(agent_id)
|
||||||
return (animated_text, "[dim]Send message to resume[/dim]", True)
|
return (animated_text, keymap_text("Send message to resume"), True)
|
||||||
|
|
||||||
if status == "running":
|
if status == "running":
|
||||||
verb = (
|
verb = (
|
||||||
@@ -679,9 +689,9 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
else "Initializing Agent"
|
else "Initializing Agent"
|
||||||
)
|
)
|
||||||
animated_text = self._get_animated_verb_text(agent_id, verb)
|
animated_text = self._get_animated_verb_text(agent_id, verb)
|
||||||
return (animated_text, "[dim]ESC to stop | CTRL-C to quit and save[/dim]", True)
|
return (animated_text, keymap_text("ESC to stop | CTRL-C to quit and save"), True)
|
||||||
|
|
||||||
return ("", "", False)
|
return (None, Text(), False)
|
||||||
|
|
||||||
def _update_agent_status_display(self) -> None:
|
def _update_agent_status_display(self) -> None:
|
||||||
try:
|
try:
|
||||||
@@ -738,7 +748,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
|
|
||||||
stats_panel = Panel(
|
stats_panel = Panel(
|
||||||
stats_content,
|
stats_content,
|
||||||
border_style="#22c55e",
|
border_style="#333333",
|
||||||
padding=(0, 1),
|
padding=(0, 1),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -793,17 +803,9 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def _get_animated_waiting_text(self, agent_id: str) -> Text:
|
def _get_animated_waiting_text(self, agent_id: str) -> Text: # noqa: ARG002
|
||||||
if agent_id not in self._agent_dot_states:
|
|
||||||
self._agent_dot_states[agent_id] = 0.0
|
|
||||||
|
|
||||||
shine_pos = self._agent_dot_states[agent_id]
|
|
||||||
word = "Waiting"
|
|
||||||
text = Text()
|
text = Text()
|
||||||
for i, char in enumerate(word):
|
text.append("Waiting", style="#fbbf24")
|
||||||
dist = abs(i - shine_pos)
|
|
||||||
text.append(char, style=self._get_shine_style(dist))
|
|
||||||
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def _start_dot_animation(self) -> None:
|
def _start_dot_animation(self) -> None:
|
||||||
@@ -957,7 +959,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
}
|
}
|
||||||
|
|
||||||
status_icon = status_indicators.get(status, "🔵")
|
status_icon = status_indicators.get(status, "🔵")
|
||||||
agent_name = f"{status_icon} {escape_markup(agent_name_raw)}"
|
agent_name = f"{status_icon} {agent_name_raw}"
|
||||||
|
|
||||||
if status in ["running", "waiting"]:
|
if status in ["running", "waiting"]:
|
||||||
self._start_agent_verb_timer(agent_id)
|
self._start_agent_verb_timer(agent_id)
|
||||||
@@ -1025,7 +1027,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
}
|
}
|
||||||
|
|
||||||
status_icon = status_indicators.get(status, "🔵")
|
status_icon = status_indicators.get(status, "🔵")
|
||||||
agent_name = f"{status_icon} {escape_markup(agent_name_raw)}"
|
agent_name = f"{status_icon} {agent_name_raw}"
|
||||||
|
|
||||||
new_node = new_parent.add(
|
new_node = new_parent.add(
|
||||||
agent_name,
|
agent_name,
|
||||||
@@ -1071,23 +1073,23 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
parent_node.allow_expand = True
|
parent_node.allow_expand = True
|
||||||
self._expand_all_agent_nodes()
|
self._expand_all_agent_nodes()
|
||||||
|
|
||||||
def _render_chat_content(self, msg_data: dict[str, Any]) -> str:
|
def _render_chat_content(self, msg_data: dict[str, Any]) -> Text | None:
|
||||||
role = msg_data.get("role")
|
role = msg_data.get("role")
|
||||||
content = msg_data.get("content", "")
|
content = msg_data.get("content", "")
|
||||||
|
|
||||||
if not content:
|
if not content:
|
||||||
return ""
|
return None
|
||||||
|
|
||||||
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(escape_markup(content))
|
return UserMessageRenderer.render_simple(content)
|
||||||
|
|
||||||
from strix.interface.tool_components.agent_message_renderer import AgentMessageRenderer
|
from strix.interface.tool_components.agent_message_renderer import AgentMessageRenderer
|
||||||
|
|
||||||
return AgentMessageRenderer.render_simple(content)
|
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]) -> Text | None:
|
||||||
tool_name = tool_data.get("tool_name", "Unknown Tool")
|
tool_name = tool_data.get("tool_name", "Unknown Tool")
|
||||||
args = tool_data.get("args", {})
|
args = tool_data.get("args", {})
|
||||||
status = tool_data.get("status", "unknown")
|
status = tool_data.get("status", "unknown")
|
||||||
@@ -1099,42 +1101,57 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
|
|
||||||
if renderer:
|
if renderer:
|
||||||
widget = renderer.render(tool_data)
|
widget = renderer.render(tool_data)
|
||||||
content = str(widget.renderable)
|
renderable = widget.renderable
|
||||||
elif tool_name == "llm_error_details":
|
if isinstance(renderable, Text):
|
||||||
lines = ["[red]✗ LLM Request Failed[/red]"]
|
return renderable
|
||||||
|
text = Text()
|
||||||
|
text.append(str(renderable))
|
||||||
|
return text
|
||||||
|
|
||||||
|
text = Text()
|
||||||
|
|
||||||
|
if tool_name == "llm_error_details":
|
||||||
|
text.append("✗ LLM Request Failed", style="red")
|
||||||
if args.get("details"):
|
if args.get("details"):
|
||||||
details = args["details"]
|
details = str(args["details"])
|
||||||
if len(details) > 300:
|
if len(details) > 300:
|
||||||
details = details[:297] + "..."
|
details = details[:297] + "..."
|
||||||
lines.append(f"[dim]Details:[/dim] {escape_markup(details)}")
|
text.append("\nDetails: ", style="dim")
|
||||||
content = "\n".join(lines)
|
text.append(details)
|
||||||
else:
|
return text
|
||||||
status_icons = {
|
|
||||||
"running": "[yellow]●[/yellow]",
|
|
||||||
"completed": "[green]✓[/green]",
|
|
||||||
"failed": "[red]✗[/red]",
|
|
||||||
"error": "[red]✗[/red]",
|
|
||||||
}
|
|
||||||
status_icon = status_icons.get(status, "[dim]○[/dim]")
|
|
||||||
|
|
||||||
lines = [f"→ Using tool [bold blue]{escape_markup(tool_name)}[/] {status_icon}"]
|
text.append("→ Using tool ")
|
||||||
|
text.append(tool_name, style="bold blue")
|
||||||
|
|
||||||
|
status_styles = {
|
||||||
|
"running": ("●", "yellow"),
|
||||||
|
"completed": ("✓", "green"),
|
||||||
|
"failed": ("✗", "red"),
|
||||||
|
"error": ("✗", "red"),
|
||||||
|
}
|
||||||
|
icon, style = status_styles.get(status, ("○", "dim"))
|
||||||
|
text.append(" ")
|
||||||
|
text.append(icon, style=style)
|
||||||
|
|
||||||
if args:
|
if args:
|
||||||
for k, v in list(args.items())[:2]:
|
for k, v in list(args.items())[:2]:
|
||||||
str_v = str(v)
|
str_v = str(v)
|
||||||
if len(str_v) > 80:
|
if len(str_v) > 80:
|
||||||
str_v = str_v[:77] + "..."
|
str_v = str_v[:77] + "..."
|
||||||
lines.append(f" [dim]{k}:[/] {escape_markup(str_v)}")
|
text.append("\n ")
|
||||||
|
text.append(k, style="dim")
|
||||||
|
text.append(": ")
|
||||||
|
text.append(str_v)
|
||||||
|
|
||||||
if status in ["completed", "failed", "error"] and result:
|
if status in ["completed", "failed", "error"] and result:
|
||||||
result_str = str(result)
|
result_str = str(result)
|
||||||
if len(result_str) > 150:
|
if len(result_str) > 150:
|
||||||
result_str = result_str[:147] + "..."
|
result_str = result_str[:147] + "..."
|
||||||
lines.append(f"[bold]Result:[/] {escape_markup(result_str)}")
|
text.append("\n")
|
||||||
|
text.append("Result: ", style="bold")
|
||||||
|
text.append(result_str)
|
||||||
|
|
||||||
content = "\n".join(lines)
|
return text
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
@on(Tree.NodeHighlighted) # type: ignore[misc]
|
@on(Tree.NodeHighlighted) # type: ignore[misc]
|
||||||
def handle_tree_highlight(self, event: Tree.NodeHighlighted) -> None:
|
def handle_tree_highlight(self, event: Tree.NodeHighlighted) -> None:
|
||||||
@@ -1322,19 +1339,6 @@ class StrixTUIApp(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 Exception: # noqa: BLE001
|
|
||||||
try:
|
|
||||||
safe_text = Text.from_markup(content)
|
|
||||||
widget.update(safe_text)
|
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
import re
|
|
||||||
|
|
||||||
plain_text = re.sub(r"\[.*?\]", "", content)
|
|
||||||
widget.update(plain_text)
|
|
||||||
|
|
||||||
def on_resize(self, event: events.Resize) -> None:
|
def on_resize(self, event: events.Resize) -> None:
|
||||||
if self.show_splash or not self.is_mounted:
|
if self.show_splash or not self.is_mounted:
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user