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
This commit is contained in:
0xallam
2026-01-08 15:02:47 -08:00
committed by Ahmed Allam
parent cdf3cca3b7
commit e8662fbda9
4 changed files with 74 additions and 52 deletions

View File

@@ -347,8 +347,6 @@ VulnerabilityDetailScreen {
.notes-tool, .notes-tool,
.thinking-tool, .thinking-tool,
.web-search-tool, .web-search-tool,
.finish-tool,
.reporting-tool,
.scan-info-tool, .scan-info-tool,
.subagent-info-tool { .subagent-info-tool {
margin: 0 !important; margin: 0 !important;
@@ -357,6 +355,14 @@ VulnerabilityDetailScreen {
background: transparent; 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-completed,
.browser-tool.status-running, .browser-tool.status-running,
.terminal-tool.status-completed, .terminal-tool.status-completed,
@@ -375,10 +381,6 @@ VulnerabilityDetailScreen {
.thinking-tool.status-running, .thinking-tool.status-running,
.web-search-tool.status-completed, .web-search-tool.status-completed,
.web-search-tool.status-running, .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-completed,
.scan-info-tool.status-running, .scan-info-tool.status-running,
.subagent-info-tool.status-completed, .subagent-info-tool.status-completed,
@@ -389,6 +391,16 @@ VulnerabilityDetailScreen {
margin-bottom: 0 !important; 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 { Tree {
background: transparent; background: transparent;
color: #e7e5e4; color: #e7e5e4;

View File

@@ -1,5 +1,6 @@
from typing import Any, ClassVar from typing import Any, ClassVar
from rich.padding import Padding
from rich.text import Text from rich.text import Text
from textual.widgets import Static from textual.widgets import Static
@@ -8,6 +9,7 @@ from .registry import register_tool_renderer
FIELD_STYLE = "bold #4ade80" FIELD_STYLE = "bold #4ade80"
BG_COLOR = "#141414"
@register_tool_renderer @register_tool_renderer
@@ -56,5 +58,7 @@ class FinishScanRenderer(BaseToolRenderer):
text.append("\n ") text.append("\n ")
text.append("Generating final report...", style="dim") text.append("Generating final report...", style="dim")
padded = Padding(text, 2, style=f"on {BG_COLOR}")
css_classes = cls.get_css_classes("completed") css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes) return Static(padded, classes=css_classes)

View File

@@ -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.padding import Padding
from rich.text import Text from rich.text import Text
from textual.widgets import Static from textual.widgets import Static
@@ -17,6 +18,7 @@ def _get_style_colors() -> dict[Any, str]:
FIELD_STYLE = "bold #4ade80" FIELD_STYLE = "bold #4ade80"
BG_COLOR = "#141414"
@register_tool_renderer @register_tool_renderer
@@ -213,5 +215,7 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
text.append("\n ") text.append("\n ")
text.append("Creating report...", style="dim") text.append("Creating report...", style="dim")
padded = Padding(text, 2, style=f"on {BG_COLOR}")
css_classes = cls.get_css_classes("completed") css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes) return Static(padded, classes=css_classes)

View File

@@ -947,7 +947,7 @@ class StrixTUIApp(App): # type: ignore[misc]
def _get_chat_content( def _get_chat_content(
self, self,
) -> tuple[Text | None, str | None]: ) -> tuple[Any, str | None]:
if not self.selected_agent_id: if not self.selected_agent_id:
return self._get_chat_placeholder_content( return self._get_chat_placeholder_content(
"Select an agent from the tree to see its activity.", "placeholder-no-agent" "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) text.append(message)
return text, f"chat-placeholder {placeholder_class}" return text, f"chat-placeholder {placeholder_class}"
def _get_rendered_events_content(self, events: list[dict[str, Any]]) -> Text: def _get_rendered_events_content(self, events: list[dict[str, Any]]) -> Any:
result = Text() renderables: list[Any] = []
if not events: if not events:
return result return Text()
first = True
for event in events: for event in events:
content: Text | None = None content: Any = None
if event["type"] == "chat": if event["type"] == "chat":
content = self._render_chat_content(event["data"]) 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"]) content = self._render_tool_content_simple(event["data"])
if content: if content:
if not first: if renderables:
result.append("\n\n") renderables.append(Text("\n"))
result.append_text(content) renderables.append(content)
first = False
if self.selected_agent_id: if self.selected_agent_id:
streaming = self.tracer.get_streaming_content(self.selected_agent_id) streaming = self.tracer.get_streaming_content(self.selected_agent_id)
if streaming: if streaming:
streaming_text = self._render_streaming_content(streaming) streaming_text = self._render_streaming_content(streaming)
if streaming_text: if streaming_text:
if not first: if renderables:
result.append("\n\n") renderables.append(Text("\n"))
result.append_text(streaming_text) 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 from strix.interface.streaming_parser import parse_streaming_content
result = Text() renderables: list[Any] = []
segments = parse_streaming_content(content) segments = parse_streaming_content(content)
for i, segment in enumerate(segments): for segment in segments:
if i > 0:
result.append("\n\n")
if segment.type == "text": if segment.type == "text":
from strix.interface.tool_components.agent_message_renderer import ( from strix.interface.tool_components.agent_message_renderer import (
AgentMessageRenderer, AgentMessageRenderer,
) )
text_content = AgentMessageRenderer.render_simple(segment.content) 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": elif segment.type == "tool":
tool_text = self._render_streaming_tool( tool_renderable = self._render_streaming_tool(
segment.tool_name or "unknown", segment.tool_name or "unknown",
segment.args or {}, segment.args or {},
segment.is_complete, 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( def _render_streaming_tool(
self, tool_name: str, args: dict[str, str], is_complete: bool self, tool_name: str, args: dict[str, str], is_complete: bool
) -> Text: ) -> Any:
from strix.interface.tool_components.registry import get_tool_renderer from strix.interface.tool_components.registry import get_tool_renderer
tool_data = { tool_data = {
@@ -1080,12 +1091,7 @@ class StrixTUIApp(App): # type: ignore[misc]
renderer = get_tool_renderer(tool_name) renderer = get_tool_renderer(tool_name)
if renderer: if renderer:
widget = renderer.render(tool_data) widget = renderer.render(tool_data)
renderable = widget.renderable return widget.renderable
if isinstance(renderable, Text):
return renderable
text = Text()
text.append(str(renderable))
return text
return self._render_default_streaming_tool(tool_name, args, is_complete) 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.allow_expand = True
parent_node.expand() 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") role = msg_data.get("role")
content = msg_data.get("content", "") content = msg_data.get("content", "")
metadata = msg_data.get("metadata", {}) metadata = msg_data.get("metadata", {})
@@ -1596,17 +1602,18 @@ class StrixTUIApp(App): # type: ignore[misc]
return UserMessageRenderer.render_simple(content) return UserMessageRenderer.render_simple(content)
if metadata.get("interrupted"): if metadata.get("interrupted"):
result = self._render_streaming_content(content) streaming_result = self._render_streaming_content(content)
result.append("\n") interrupted_text = Text()
result.append("", style="yellow") interrupted_text.append("\n")
result.append("Interrupted by user", style="yellow dim") interrupted_text.append("", style="yellow")
return result 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 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]) -> Text | None: def _render_tool_content_simple(self, tool_data: dict[str, Any]) -> Any:
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")
@@ -1618,12 +1625,7 @@ class StrixTUIApp(App): # type: ignore[misc]
if renderer: if renderer:
widget = renderer.render(tool_data) widget = renderer.render(tool_data)
renderable = widget.renderable return widget.renderable
if isinstance(renderable, Text):
return renderable
text = Text()
text.append(str(renderable))
return text
text = Text() text = Text()