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:
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user