From 6422bfa0b4b02a66de478e521593d35f9f4fa031 Mon Sep 17 00:00:00 2001 From: 0xallam Date: Tue, 6 Jan 2026 12:14:40 -0800 Subject: [PATCH] feat(tui): show tool output in terminal and python renderers - Terminal renderer now displays command output with smart filtering - Strips PS1 prompts, command echoes, and hardcoded status messages - Python renderer now shows stdout/stderr from execution results - Both renderers support line truncation (50 lines max, 200 chars/line) - Removed smart coloring in favor of consistent dim styling - Added proper error and exit code display --- .../tool_components/python_renderer.py | 93 ++++++++++- .../tool_components/terminal_renderer.py | 156 +++++++++++++++++- 2 files changed, 238 insertions(+), 11 deletions(-) diff --git a/strix/interface/tool_components/python_renderer.py b/strix/interface/tool_components/python_renderer.py index 8fb54ac..f20e036 100644 --- a/strix/interface/tool_components/python_renderer.py +++ b/strix/interface/tool_components/python_renderer.py @@ -1,3 +1,4 @@ +import re from functools import cache from typing import Any, ClassVar @@ -10,6 +11,14 @@ from .base_renderer import BaseToolRenderer from .registry import register_tool_renderer +MAX_OUTPUT_LINES = 50 +MAX_LINE_LENGTH = 200 + +STRIP_PATTERNS = [ + r"\.\.\. \[(stdout|stderr|result|output|error) truncated at \d+k? chars\]", +] + + @cache def _get_style_colors() -> dict[Any, str]: style = get_style_by_name("native") @@ -43,29 +52,99 @@ class PythonRenderer(BaseToolRenderer): return text + @classmethod + def _clean_output(cls, output: str) -> str: + cleaned = output + for pattern in STRIP_PATTERNS: + cleaned = re.sub(pattern, "", cleaned) + return cleaned.strip() + + @classmethod + def _truncate_line(cls, line: str) -> str: + if len(line) > MAX_LINE_LENGTH: + return line[: MAX_LINE_LENGTH - 3] + "..." + return line + + @classmethod + def _format_output(cls, output: str) -> Text: + text = Text() + lines = output.splitlines() + total_lines = len(lines) + + head_count = MAX_OUTPUT_LINES // 2 + tail_count = MAX_OUTPUT_LINES - head_count - 1 + + if total_lines <= MAX_OUTPUT_LINES: + display_lines = lines + truncated = False + hidden_count = 0 + else: + display_lines = lines[:head_count] + truncated = True + hidden_count = total_lines - head_count - tail_count + + for i, line in enumerate(display_lines): + truncated_line = cls._truncate_line(line) + text.append(" ") + text.append(truncated_line, style="dim") + if i < len(display_lines) - 1 or truncated: + text.append("\n") + + if truncated: + text.append(f" ... {hidden_count} lines truncated ...", style="dim italic") + text.append("\n") + tail_lines = lines[-tail_count:] + for i, line in enumerate(tail_lines): + truncated_line = cls._truncate_line(line) + text.append(" ") + text.append(truncated_line, style="dim") + if i < len(tail_lines) - 1: + text.append("\n") + + return text + + @classmethod + def _append_output(cls, text: Text, result: dict[str, Any]) -> None: + stdout = result.get("stdout", "") + stderr = result.get("stderr", "") + + stdout = cls._clean_output(stdout) if stdout else "" + stderr = cls._clean_output(stderr) if stderr else "" + + if stdout: + text.append("\n") + formatted_output = cls._format_output(stdout) + text.append_text(formatted_output) + + if stderr: + text.append("\n") + text.append(" stderr: ", style="bold #ef4444") + formatted_stderr = cls._format_output(stderr) + text.append_text(formatted_stderr) + @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") action = args.get("action", "") code = args.get("code", "") text = Text() - text.append(" ") - text.append("Python", style="bold #3b82f6") - text.append("\n") + text.append(" ", style="dim") if code and action in ["new_session", "execute"]: text.append_text(cls._highlight_python(code)) elif action == "close": - text.append(" ") text.append("Closing session...", style="dim") elif action == "list_sessions": - text.append(" ") text.append("Listing sessions...", style="dim") else: - text.append(" ") text.append("Running...", style="dim") - css_classes = cls.get_css_classes("completed") + if result and isinstance(result, dict): + cls._append_output(text, result) + + css_classes = cls.get_css_classes(status) return Static(text, classes=css_classes) diff --git a/strix/interface/tool_components/terminal_renderer.py b/strix/interface/tool_components/terminal_renderer.py index f707a00..f6334a5 100644 --- a/strix/interface/tool_components/terminal_renderer.py +++ b/strix/interface/tool_components/terminal_renderer.py @@ -1,3 +1,4 @@ +import re from functools import cache from typing import Any, ClassVar @@ -10,6 +11,23 @@ from .base_renderer import BaseToolRenderer from .registry import register_tool_renderer +MAX_OUTPUT_LINES = 50 +MAX_LINE_LENGTH = 200 + +STRIP_PATTERNS = [ + ( + r"\n?\[Command still running after [\d.]+s - showing output so far\.?" + r"\s*(?:Use C-c to interrupt if needed\.)?\]" + ), + r"^\[Below is the output of the previous command\.\]\n?", + r"^No command is currently running\. Cannot send input\.$", + ( + r"^A command is already running\. Use is_input=true to send input to it, " + r"or interrupt it first \(e\.g\., with C-c\)\.$" + ), +] + + @cache def _get_style_colors() -> dict[Any, str]: style = get_style_by_name("native") @@ -110,24 +128,29 @@ class TerminalRenderer(BaseToolRenderer): 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) - content = cls._build_content(command, is_input) + content = cls._build_content(command, is_input, status, result) css_classes = cls.get_css_classes(status) return Static(content, classes=css_classes) @classmethod - def _build_content(cls, command: str, is_input: bool) -> Text: + def _build_content( + cls, command: str, is_input: bool, status: str, result: dict[str, Any] | None + ) -> Text: text = Text() terminal_icon = ">_" if not command.strip(): - text.append(terminal_icon) + text.append(terminal_icon, style="dim") text.append(" ") text.append("getting logs...", style="dim") + if result: + cls._append_output(text, result, status, command) return text is_special = ( @@ -136,7 +159,7 @@ class TerminalRenderer(BaseToolRenderer): or command.startswith(("M-", "S-", "C-S-", "C-M-", "S-M-")) ) - text.append(terminal_icon) + text.append(terminal_icon, style="dim") text.append(" ") if is_special: @@ -150,8 +173,133 @@ class TerminalRenderer(BaseToolRenderer): text.append(" ") text.append_text(cls._format_command(command)) + if result: + cls._append_output(text, result, status, command) + return text + @classmethod + def _clean_output(cls, output: str, command: str = "") -> str: + cleaned = output + + for pattern in STRIP_PATTERNS: + cleaned = re.sub(pattern, "", cleaned, flags=re.MULTILINE) + + if cleaned.strip(): + lines = cleaned.splitlines() + filtered_lines: list[str] = [] + for line in lines: + if not filtered_lines and not line.strip(): + continue + if re.match(r"^\[STRIX_\d+\]\$\s*", line): + continue + if command and line.strip() == command.strip(): + continue + if command and re.match(r"^[\$#>]\s*" + re.escape(command.strip()) + r"\s*$", line): + continue + filtered_lines.append(line) + + while filtered_lines and re.match(r"^\[STRIX_\d+\]\$\s*", filtered_lines[-1]): + filtered_lines.pop() + + cleaned = "\n".join(filtered_lines) + + return cleaned.strip() + + @classmethod + def _append_output( + cls, text: Text, result: dict[str, Any], tool_status: str, command: str = "" + ) -> None: + raw_output = result.get("content", "") + output = cls._clean_output(raw_output, command) + error = result.get("error") + exit_code = result.get("exit_code") + result_status = result.get("status", "") + + if error and not cls._is_status_message(error): + text.append("\n") + text.append(" error: ", style="bold #ef4444") + text.append(cls._truncate_line(error), style="#ef4444") + return + + if result_status == "running" or tool_status == "running": + if output and output.strip(): + text.append("\n") + formatted_output = cls._format_output(output) + text.append_text(formatted_output) + return + + if not output or not output.strip(): + if exit_code is not None and exit_code != 0: + text.append("\n") + text.append(f" exit {exit_code}", style="dim #ef4444") + return + + text.append("\n") + formatted_output = cls._format_output(output) + text.append_text(formatted_output) + + if exit_code is not None and exit_code != 0: + text.append("\n") + text.append(f" exit {exit_code}", style="dim #ef4444") + + @classmethod + def _is_status_message(cls, message: str) -> bool: + status_patterns = [ + r"No command is currently running", + r"A command is already running", + r"Cannot send input", + r"Use is_input=true", + r"Use C-c to interrupt", + r"showing output so far", + ] + return any(re.search(pattern, message) for pattern in status_patterns) + + @classmethod + def _format_output(cls, output: str) -> Text: + text = Text() + lines = output.splitlines() + total_lines = len(lines) + + head_count = MAX_OUTPUT_LINES // 2 + tail_count = MAX_OUTPUT_LINES - head_count - 1 + + if total_lines <= MAX_OUTPUT_LINES: + display_lines = lines + truncated = False + hidden_count = 0 + else: + display_lines = lines[:head_count] + truncated = True + hidden_count = total_lines - head_count - tail_count + + for i, line in enumerate(display_lines): + truncated_line = cls._truncate_line(line) + text.append(" ") + text.append(truncated_line, style="dim") + if i < len(display_lines) - 1 or truncated: + text.append("\n") + + if truncated: + text.append(f" ... {hidden_count} lines truncated ...", style="dim italic") + text.append("\n") + tail_lines = lines[-tail_count:] + for i, line in enumerate(tail_lines): + truncated_line = cls._truncate_line(line) + text.append(" ") + text.append(truncated_line, style="dim") + if i < len(tail_lines) - 1: + text.append("\n") + + return text + + @classmethod + def _truncate_line(cls, line: str) -> str: + clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) + if len(clean_line) > MAX_LINE_LENGTH: + return line[: MAX_LINE_LENGTH - 3] + "..." + return line + @classmethod def _format_command(cls, command: str) -> Text: return cls._highlight_bash(command)