From e8662fbda9b5786535b934d22a78fc502b572f20 Mon Sep 17 00:00:00 2001 From: 0xallam Date: Thu, 8 Jan 2026 15:02:47 -0800 Subject: [PATCH] Add background styling to finish and reporting tool renderers - Wrap finish_scan and create_vulnerability_report tool output in Padding with dark grey background (#141414) - Refactor TUI rendering to support heterogeneous renderables (Text, Padding, Group) instead of just Text - Update _render_streaming_content and _render_tool_content_simple to return Any renderable type - Handle interrupted messages by composing with Group instead of appending to Text --- strix/interface/assets/tui_styles.tcss | 24 +++-- .../tool_components/finish_renderer.py | 6 +- .../tool_components/reporting_renderer.py | 6 +- strix/interface/tui.py | 90 ++++++++++--------- 4 files changed, 74 insertions(+), 52 deletions(-) diff --git a/strix/interface/assets/tui_styles.tcss b/strix/interface/assets/tui_styles.tcss index 8c944c3..f800c5d 100644 --- a/strix/interface/assets/tui_styles.tcss +++ b/strix/interface/assets/tui_styles.tcss @@ -347,8 +347,6 @@ VulnerabilityDetailScreen { .notes-tool, .thinking-tool, .web-search-tool, -.finish-tool, -.reporting-tool, .scan-info-tool, .subagent-info-tool { margin: 0 !important; @@ -357,6 +355,14 @@ VulnerabilityDetailScreen { background: transparent; } +.finish-tool, +.reporting-tool { + margin: 0 !important; + margin-top: 0 !important; + margin-bottom: 0 !important; + background: transparent; +} + .browser-tool.status-completed, .browser-tool.status-running, .terminal-tool.status-completed, @@ -375,10 +381,6 @@ VulnerabilityDetailScreen { .thinking-tool.status-running, .web-search-tool.status-completed, .web-search-tool.status-running, -.finish-tool.status-completed, -.finish-tool.status-running, -.reporting-tool.status-completed, -.reporting-tool.status-running, .scan-info-tool.status-completed, .scan-info-tool.status-running, .subagent-info-tool.status-completed, @@ -389,6 +391,16 @@ VulnerabilityDetailScreen { margin-bottom: 0 !important; } +.finish-tool.status-completed, +.finish-tool.status-running, +.reporting-tool.status-completed, +.reporting-tool.status-running { + background: transparent; + margin: 0 !important; + margin-top: 0 !important; + margin-bottom: 0 !important; +} + Tree { background: transparent; color: #e7e5e4; diff --git a/strix/interface/tool_components/finish_renderer.py b/strix/interface/tool_components/finish_renderer.py index 87cbc21..22beadd 100644 --- a/strix/interface/tool_components/finish_renderer.py +++ b/strix/interface/tool_components/finish_renderer.py @@ -1,5 +1,6 @@ from typing import Any, ClassVar +from rich.padding import Padding from rich.text import Text from textual.widgets import Static @@ -8,6 +9,7 @@ from .registry import register_tool_renderer FIELD_STYLE = "bold #4ade80" +BG_COLOR = "#141414" @register_tool_renderer @@ -56,5 +58,7 @@ class FinishScanRenderer(BaseToolRenderer): text.append("\n ") text.append("Generating final report...", style="dim") + padded = Padding(text, 2, style=f"on {BG_COLOR}") + css_classes = cls.get_css_classes("completed") - return Static(text, classes=css_classes) + return Static(padded, classes=css_classes) diff --git a/strix/interface/tool_components/reporting_renderer.py b/strix/interface/tool_components/reporting_renderer.py index 8293bc0..8a2f045 100644 --- a/strix/interface/tool_components/reporting_renderer.py +++ b/strix/interface/tool_components/reporting_renderer.py @@ -3,6 +3,7 @@ from typing import Any, ClassVar from pygments.lexers import PythonLexer from pygments.styles import get_style_by_name +from rich.padding import Padding from rich.text import Text from textual.widgets import Static @@ -17,6 +18,7 @@ def _get_style_colors() -> dict[Any, str]: FIELD_STYLE = "bold #4ade80" +BG_COLOR = "#141414" @register_tool_renderer @@ -213,5 +215,7 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer): text.append("\n ") text.append("Creating report...", style="dim") + padded = Padding(text, 2, style=f"on {BG_COLOR}") + css_classes = cls.get_css_classes("completed") - return Static(text, classes=css_classes) + return Static(padded, classes=css_classes) diff --git a/strix/interface/tui.py b/strix/interface/tui.py index 495f003..27ed2d5 100644 --- a/strix/interface/tui.py +++ b/strix/interface/tui.py @@ -947,7 +947,7 @@ class StrixTUIApp(App): # type: ignore[misc] def _get_chat_content( self, - ) -> tuple[Text | None, str | None]: + ) -> tuple[Any, str | None]: if not self.selected_agent_id: return self._get_chat_placeholder_content( "Select an agent from the tree to see its activity.", "placeholder-no-agent" @@ -1005,15 +1005,14 @@ class StrixTUIApp(App): # type: ignore[misc] 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]]) -> Any: + renderables: list[Any] = [] if not events: - return result + return Text() - first = True for event in events: - content: Text | None = None + content: Any = None if event["type"] == "chat": content = self._render_chat_content(event["data"]) @@ -1021,53 +1020,65 @@ class StrixTUIApp(App): # type: ignore[misc] content = self._render_tool_content_simple(event["data"]) if content: - if not first: - result.append("\n\n") - result.append_text(content) - first = False + if renderables: + renderables.append(Text("\n")) + renderables.append(content) if self.selected_agent_id: streaming = self.tracer.get_streaming_content(self.selected_agent_id) if streaming: streaming_text = self._render_streaming_content(streaming) if streaming_text: - if not first: - result.append("\n\n") - result.append_text(streaming_text) + if renderables: + renderables.append(Text("\n")) + renderables.append(streaming_text) - return result + if not renderables: + return Text() - def _render_streaming_content(self, content: str) -> Text: + if len(renderables) == 1: + return renderables[0] + + return Group(*renderables) + + def _render_streaming_content(self, content: str) -> Any: from strix.interface.streaming_parser import parse_streaming_content - result = Text() + renderables: list[Any] = [] segments = parse_streaming_content(content) - for i, segment in enumerate(segments): - if i > 0: - result.append("\n\n") - + for segment in segments: if segment.type == "text": from strix.interface.tool_components.agent_message_renderer import ( AgentMessageRenderer, ) text_content = AgentMessageRenderer.render_simple(segment.content) - result.append_text(text_content) + if renderables: + renderables.append(Text("\n")) + renderables.append(text_content) elif segment.type == "tool": - tool_text = self._render_streaming_tool( + tool_renderable = self._render_streaming_tool( segment.tool_name or "unknown", segment.args or {}, segment.is_complete, ) - result.append_text(tool_text) + if renderables: + renderables.append(Text("\n")) + renderables.append(tool_renderable) - return result + if not renderables: + return Text() + + if len(renderables) == 1: + return renderables[0] + + return Group(*renderables) def _render_streaming_tool( self, tool_name: str, args: dict[str, str], is_complete: bool - ) -> Text: + ) -> Any: from strix.interface.tool_components.registry import get_tool_renderer tool_data = { @@ -1080,12 +1091,7 @@ class StrixTUIApp(App): # type: ignore[misc] renderer = get_tool_renderer(tool_name) if renderer: widget = renderer.render(tool_data) - renderable = widget.renderable - if isinstance(renderable, Text): - return renderable - text = Text() - text.append(str(renderable)) - return text + return widget.renderable return self._render_default_streaming_tool(tool_name, args, is_complete) @@ -1582,7 +1588,7 @@ class StrixTUIApp(App): # type: ignore[misc] parent_node.allow_expand = True parent_node.expand() - def _render_chat_content(self, msg_data: dict[str, Any]) -> Text | None: + def _render_chat_content(self, msg_data: dict[str, Any]) -> Any: role = msg_data.get("role") content = msg_data.get("content", "") metadata = msg_data.get("metadata", {}) @@ -1596,17 +1602,18 @@ class StrixTUIApp(App): # type: ignore[misc] return UserMessageRenderer.render_simple(content) if metadata.get("interrupted"): - result = self._render_streaming_content(content) - result.append("\n") - result.append("⚠ ", style="yellow") - result.append("Interrupted by user", style="yellow dim") - return result + streaming_result = self._render_streaming_content(content) + interrupted_text = Text() + interrupted_text.append("\n") + interrupted_text.append("⚠ ", style="yellow") + interrupted_text.append("Interrupted by user", style="yellow dim") + return Group(streaming_result, interrupted_text) from strix.interface.tool_components.agent_message_renderer import AgentMessageRenderer return AgentMessageRenderer.render_simple(content) - def _render_tool_content_simple(self, tool_data: dict[str, Any]) -> Text | None: + def _render_tool_content_simple(self, tool_data: dict[str, Any]) -> Any: tool_name = tool_data.get("tool_name", "Unknown Tool") args = tool_data.get("args", {}) status = tool_data.get("status", "unknown") @@ -1618,12 +1625,7 @@ class StrixTUIApp(App): # type: ignore[misc] if renderer: widget = renderer.render(tool_data) - renderable = widget.renderable - if isinstance(renderable, Text): - return renderable - text = Text() - text.append(str(renderable)) - return text + return widget.renderable text = Text()