2025-12-14 04:39:28 +04:00
|
|
|
from functools import cache
|
2025-08-08 20:36:44 -07:00
|
|
|
from typing import Any, ClassVar
|
|
|
|
|
|
2025-12-14 04:39:28 +04:00
|
|
|
from pygments.lexers import get_lexer_by_name, get_lexer_for_filename
|
|
|
|
|
from pygments.styles import get_style_by_name
|
|
|
|
|
from pygments.util import ClassNotFound
|
2026-01-05 00:07:54 -08:00
|
|
|
from rich.text import Text
|
2025-08-08 20:36:44 -07:00
|
|
|
from textual.widgets import Static
|
|
|
|
|
|
|
|
|
|
from .base_renderer import BaseToolRenderer
|
|
|
|
|
from .registry import register_tool_renderer
|
|
|
|
|
|
|
|
|
|
|
2025-12-14 04:39:28 +04:00
|
|
|
@cache
|
|
|
|
|
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"]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_lexer_for_file(path: str) -> Any:
|
|
|
|
|
try:
|
|
|
|
|
return get_lexer_for_filename(path)
|
|
|
|
|
except ClassNotFound:
|
|
|
|
|
return get_lexer_by_name("text")
|
|
|
|
|
|
|
|
|
|
|
2025-08-08 20:36:44 -07:00
|
|
|
@register_tool_renderer
|
|
|
|
|
class StrReplaceEditorRenderer(BaseToolRenderer):
|
|
|
|
|
tool_name: ClassVar[str] = "str_replace_editor"
|
|
|
|
|
css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"]
|
|
|
|
|
|
2025-12-14 04:39:28 +04:00
|
|
|
@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
|
2026-01-05 00:07:54 -08:00
|
|
|
def _highlight_code(cls, code: str, path: str) -> Text:
|
2025-12-14 04:39:28 +04:00
|
|
|
lexer = _get_lexer_for_file(path)
|
2026-01-05 00:07:54 -08:00
|
|
|
text = Text()
|
2025-12-14 04:39:28 +04:00
|
|
|
|
|
|
|
|
for token_type, token_value in lexer.get_tokens(code):
|
|
|
|
|
if not token_value:
|
|
|
|
|
continue
|
|
|
|
|
color = cls._get_token_color(token_type)
|
2026-01-05 00:07:54 -08:00
|
|
|
text.append(token_value, style=color)
|
2025-12-14 04:39:28 +04:00
|
|
|
|
2026-01-05 00:07:54 -08:00
|
|
|
return text
|
2025-12-14 04:39:28 +04:00
|
|
|
|
2025-08-08 20:36:44 -07:00
|
|
|
@classmethod
|
|
|
|
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
|
|
|
args = tool_data.get("args", {})
|
|
|
|
|
result = tool_data.get("result")
|
|
|
|
|
|
|
|
|
|
command = args.get("command", "")
|
|
|
|
|
path = args.get("path", "")
|
2025-12-14 04:39:28 +04:00
|
|
|
old_str = args.get("old_str", "")
|
|
|
|
|
new_str = args.get("new_str", "")
|
|
|
|
|
file_text = args.get("file_text", "")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
2026-01-05 00:07:54 -08:00
|
|
|
text = Text()
|
|
|
|
|
|
|
|
|
|
icons_and_labels = {
|
2026-01-19 16:42:38 -08:00
|
|
|
"view": ("◇ ", "read", "#10b981"),
|
|
|
|
|
"str_replace": ("◇ ", "edit", "#10b981"),
|
|
|
|
|
"create": ("◇ ", "create", "#10b981"),
|
|
|
|
|
"insert": ("◇ ", "insert", "#10b981"),
|
|
|
|
|
"undo_edit": ("◇ ", "undo", "#10b981"),
|
2026-01-05 00:07:54 -08:00
|
|
|
}
|
2025-08-08 20:36:44 -07:00
|
|
|
|
2026-01-19 16:42:38 -08:00
|
|
|
icon, label, color = icons_and_labels.get(command, ("◇ ", "file", "#10b981"))
|
|
|
|
|
text.append(icon, style=color)
|
|
|
|
|
text.append(label, style="dim")
|
2026-01-05 00:07:54 -08:00
|
|
|
|
|
|
|
|
if path:
|
|
|
|
|
path_display = path[-60:] if len(path) > 60 else path
|
|
|
|
|
text.append(" ")
|
|
|
|
|
text.append(path_display, style="dim")
|
2025-12-14 04:39:28 +04:00
|
|
|
|
|
|
|
|
if command == "str_replace" and (old_str or new_str):
|
|
|
|
|
if old_str:
|
2026-01-05 09:52:05 -08:00
|
|
|
highlighted_old = cls._highlight_code(old_str, path)
|
2026-01-05 00:07:54 -08:00
|
|
|
for line in highlighted_old.plain.split("\n"):
|
|
|
|
|
text.append("\n")
|
|
|
|
|
text.append("-", style="#ef4444")
|
|
|
|
|
text.append(" ")
|
|
|
|
|
text.append(line)
|
|
|
|
|
|
2025-12-14 04:39:28 +04:00
|
|
|
if new_str:
|
2026-01-05 09:52:05 -08:00
|
|
|
highlighted_new = cls._highlight_code(new_str, path)
|
2026-01-05 00:07:54 -08:00
|
|
|
for line in highlighted_new.plain.split("\n"):
|
|
|
|
|
text.append("\n")
|
|
|
|
|
text.append("+", style="#22c55e")
|
|
|
|
|
text.append(" ")
|
|
|
|
|
text.append(line)
|
|
|
|
|
|
2025-12-14 04:39:28 +04:00
|
|
|
elif command == "create" and file_text:
|
2026-01-05 00:07:54 -08:00
|
|
|
text.append("\n")
|
2026-01-05 09:52:05 -08:00
|
|
|
text.append_text(cls._highlight_code(file_text, path))
|
2026-01-05 00:07:54 -08:00
|
|
|
|
2025-12-14 04:39:28 +04:00
|
|
|
elif command == "insert" and new_str:
|
2026-01-05 09:52:05 -08:00
|
|
|
highlighted_new = cls._highlight_code(new_str, path)
|
2026-01-05 00:07:54 -08:00
|
|
|
for line in highlighted_new.plain.split("\n"):
|
|
|
|
|
text.append("\n")
|
|
|
|
|
text.append("+", style="#22c55e")
|
|
|
|
|
text.append(" ")
|
|
|
|
|
text.append(line)
|
|
|
|
|
|
2026-01-09 16:32:40 -08:00
|
|
|
elif isinstance(result, str) and result.strip():
|
|
|
|
|
text.append("\n ")
|
|
|
|
|
text.append(result.strip(), style="dim")
|
2025-12-14 04:39:28 +04:00
|
|
|
elif not (result and isinstance(result, dict) and "content" in result) and not path:
|
2026-01-05 00:07:54 -08:00
|
|
|
text.append(" ")
|
|
|
|
|
text.append("Processing...", style="dim")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
|
|
|
|
css_classes = cls.get_css_classes("completed")
|
2026-01-05 00:07:54 -08:00
|
|
|
return Static(text, classes=css_classes)
|
2025-08-08 20:36:44 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@register_tool_renderer
|
|
|
|
|
class ListFilesRenderer(BaseToolRenderer):
|
|
|
|
|
tool_name: ClassVar[str] = "list_files"
|
|
|
|
|
css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"]
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
|
|
|
args = tool_data.get("args", {})
|
|
|
|
|
path = args.get("path", "")
|
|
|
|
|
|
2026-01-05 00:07:54 -08:00
|
|
|
text = Text()
|
2026-01-19 23:01:47 -08:00
|
|
|
text.append("◇ ", style="#10b981")
|
|
|
|
|
text.append("list", style="dim")
|
2026-01-05 00:07:54 -08:00
|
|
|
text.append(" ")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
|
|
|
|
if path:
|
|
|
|
|
path_display = path[-60:] if len(path) > 60 else path
|
2026-01-05 00:07:54 -08:00
|
|
|
text.append(path_display, style="dim")
|
2025-08-08 20:36:44 -07:00
|
|
|
else:
|
2026-01-05 00:07:54 -08:00
|
|
|
text.append("Current directory", style="dim")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
|
|
|
|
css_classes = cls.get_css_classes("completed")
|
2026-01-05 00:07:54 -08:00
|
|
|
return Static(text, classes=css_classes)
|
2025-08-08 20:36:44 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@register_tool_renderer
|
|
|
|
|
class SearchFilesRenderer(BaseToolRenderer):
|
|
|
|
|
tool_name: ClassVar[str] = "search_files"
|
|
|
|
|
css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"]
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
|
|
|
args = tool_data.get("args", {})
|
|
|
|
|
path = args.get("path", "")
|
|
|
|
|
regex = args.get("regex", "")
|
|
|
|
|
|
2026-01-05 00:07:54 -08:00
|
|
|
text = Text()
|
2026-01-19 16:42:38 -08:00
|
|
|
text.append("◇ ", style="#a855f7")
|
|
|
|
|
text.append("search", style="dim")
|
|
|
|
|
text.append(" ")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
|
|
|
|
if path and regex:
|
2026-01-05 09:52:05 -08:00
|
|
|
text.append(path, style="dim")
|
2026-01-19 16:42:38 -08:00
|
|
|
text.append(" ", style="dim")
|
|
|
|
|
text.append(regex, style="#a855f7")
|
2025-08-08 20:36:44 -07:00
|
|
|
elif path:
|
2026-01-05 09:52:05 -08:00
|
|
|
text.append(path, style="dim")
|
2025-08-08 20:36:44 -07:00
|
|
|
elif regex:
|
2026-01-19 16:42:38 -08:00
|
|
|
text.append(regex, style="#a855f7")
|
2025-08-08 20:36:44 -07:00
|
|
|
else:
|
2026-01-19 16:42:38 -08:00
|
|
|
text.append("...", style="dim")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
|
|
|
|
css_classes = cls.get_css_classes("completed")
|
2026-01-05 00:07:54 -08:00
|
|
|
return Static(text, classes=css_classes)
|