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,
.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;

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()