diff --git a/strix/interface/tool_components/browser_renderer.py b/strix/interface/tool_components/browser_renderer.py index 8b2d252..8185f0d 100644 --- a/strix/interface/tool_components/browser_renderer.py +++ b/strix/interface/tool_components/browser_renderer.py @@ -1,16 +1,53 @@ +from functools import cache from typing import Any, ClassVar +from pygments.lexers import get_lexer_by_name +from pygments.styles import get_style_by_name from textual.widgets import Static from .base_renderer import BaseToolRenderer from .registry import register_tool_renderer +@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"]} + + @register_tool_renderer class BrowserRenderer(BaseToolRenderer): tool_name: ClassVar[str] = "browser_action" css_classes: ClassVar[list[str]] = ["tool-call", "browser-tool"] + @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_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", {}) @@ -115,6 +152,5 @@ class BrowserRenderer(BaseToolRenderer): @classmethod def _format_js(cls, js_code: str) -> str: - if len(js_code) > 200: - js_code = js_code[:197] + "..." - return f"[white]{cls.escape_markup(js_code)}[/white]" + code_display = js_code[:2000] + "..." if len(js_code) > 2000 else js_code + return cls._highlight_js(code_display) diff --git a/strix/interface/tool_components/file_edit_renderer.py b/strix/interface/tool_components/file_edit_renderer.py index 9e2fbe3..cf37636 100644 --- a/strix/interface/tool_components/file_edit_renderer.py +++ b/strix/interface/tool_components/file_edit_renderer.py @@ -1,16 +1,61 @@ +from functools import cache from typing import Any, ClassVar +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 from textual.widgets import Static from .base_renderer import BaseToolRenderer from .registry import register_tool_renderer +@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") + + @register_tool_renderer class StrReplaceEditorRenderer(BaseToolRenderer): tool_name: ClassVar[str] = "str_replace_editor" css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"] + @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_code(cls, code: str, path: str) -> str: + lexer = _get_lexer_for_file(path) + 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", {}) @@ -18,6 +63,9 @@ class StrReplaceEditorRenderer(BaseToolRenderer): command = args.get("command", "") path = args.get("path", "") + old_str = args.get("old_str", "") + new_str = args.get("new_str", "") + file_text = args.get("file_text", "") if command == "view": header = "📖 [bold #10b981]Reading file[/]" @@ -32,12 +80,33 @@ class StrReplaceEditorRenderer(BaseToolRenderer): else: header = "📄 [bold #10b981]File operation[/]" - if (result and isinstance(result, dict) and "content" in result) or path: - path_display = path[-60:] if len(path) > 60 else path - content_text = f"{header} [dim]{cls.escape_markup(path_display)}[/]" - else: - content_text = f"{header} [dim]Processing...[/]" + path_display = path[-60:] if len(path) > 60 else path + content_parts = [f"{header} [dim]{cls.escape_markup(path_display)}[/]"] + if command == "str_replace" and (old_str or new_str): + if old_str: + old_display = old_str[:1000] + "..." if len(old_str) > 1000 else old_str + highlighted_old = cls._highlight_code(old_display, path) + old_lines = highlighted_old.split("\n") + content_parts.extend(f"[#ef4444]-[/] {line}" for line in old_lines) + if new_str: + new_display = new_str[:1000] + "..." if len(new_str) > 1000 else new_str + highlighted_new = cls._highlight_code(new_display, path) + new_lines = highlighted_new.split("\n") + content_parts.extend(f"[#22c55e]+[/] {line}" for line in new_lines) + elif command == "create" and file_text: + text_display = file_text[:1500] + "..." if len(file_text) > 1500 else file_text + highlighted_text = cls._highlight_code(text_display, path) + content_parts.append(highlighted_text) + elif command == "insert" and new_str: + new_display = new_str[:1000] + "..." if len(new_str) > 1000 else new_str + highlighted_new = cls._highlight_code(new_display, path) + new_lines = highlighted_new.split("\n") + content_parts.extend(f"[#22c55e]+[/] {line}" for line in new_lines) + elif not (result and isinstance(result, dict) and "content" in result) and not path: + content_parts = [f"{header} [dim]Processing...[/]"] + + content_text = "\n".join(content_parts) css_classes = cls.get_css_classes("completed") return Static(content_text, classes=css_classes) diff --git a/strix/interface/tool_components/python_renderer.py b/strix/interface/tool_components/python_renderer.py index 9cb8cc0..1b0d51f 100644 --- a/strix/interface/tool_components/python_renderer.py +++ b/strix/interface/tool_components/python_renderer.py @@ -1,16 +1,53 @@ +from functools import cache from typing import Any, ClassVar +from pygments.lexers import PythonLexer +from pygments.styles import get_style_by_name from textual.widgets import Static from .base_renderer import BaseToolRenderer from .registry import register_tool_renderer +@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"]} + + @register_tool_renderer class PythonRenderer(BaseToolRenderer): tool_name: ClassVar[str] = "python_action" css_classes: ClassVar[list[str]] = ["tool-call", "python-tool"] + @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_python(cls, code: str) -> str: + lexer = PythonLexer() + 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", {}) @@ -21,8 +58,9 @@ class PythonRenderer(BaseToolRenderer): header = " [bold #3b82f6]Python[/]" if code and action in ["new_session", "execute"]: - code_display = code[:600] + "..." if len(code) > 600 else code - content_text = f"{header}\n [italic white]{cls.escape_markup(code_display)}[/]" + code_display = code[:2000] + "..." if len(code) > 2000 else code + highlighted_code = cls._highlight_python(code_display) + content_text = f"{header}\n{highlighted_code}" elif action == "close": content_text = f"{header}\n [dim]Closing session...[/]" elif action == "list_sessions": diff --git a/strix/interface/tool_components/terminal_renderer.py b/strix/interface/tool_components/terminal_renderer.py index f6159cd..332852c 100644 --- a/strix/interface/tool_components/terminal_renderer.py +++ b/strix/interface/tool_components/terminal_renderer.py @@ -1,16 +1,53 @@ +from functools import cache from typing import Any, ClassVar +from pygments.lexers import get_lexer_by_name +from pygments.styles import get_style_by_name from textual.widgets import Static from .base_renderer import BaseToolRenderer from .registry import register_tool_renderer +@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"]} + + @register_tool_renderer class TerminalRenderer(BaseToolRenderer): tool_name: ClassVar[str] = "terminal_execute" css_classes: ClassVar[list[str]] = ["tool-call", "terminal-tool"] + @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) -> 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", {}) @@ -115,17 +152,15 @@ class TerminalRenderer(BaseToolRenderer): if is_input: formatted_command = cls._format_command_display(command) - return f"{terminal_icon} [#3b82f6]>>>[/] [#22c55e]{formatted_command}[/]" + return f"{terminal_icon} [#3b82f6]>>>[/] {formatted_command}" formatted_command = cls._format_command_display(command) - return f"{terminal_icon} [#22c55e]$ {formatted_command}[/]" + return f"{terminal_icon} [#22c55e]$[/] {formatted_command}" @classmethod def _format_command_display(cls, command: str) -> str: if not command: return "" - if len(command) > 400: - command = command[:397] + "..." - - return cls.escape_markup(command) + cmd_display = command[:2000] + "..." if len(command) > 2000 else command + return cls._highlight_bash(cmd_display)