greydgl--pentestgpt
203 行
6.8 KiB
Python
203 行
6.8 KiB
Python
"""Activity feed component for displaying real-time agent updates."""
|
|
|
|
from collections.abc import Iterator
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from rich.markup import escape as rich_escape
|
|
from textual.containers import VerticalScroll
|
|
from textual.widgets import Static
|
|
|
|
|
|
def escape_markup(text: str) -> str:
|
|
"""Escape Rich markup characters."""
|
|
return str(rich_escape(text))
|
|
|
|
|
|
class ActivityFeed(VerticalScroll):
|
|
"""Scrollable activity feed showing agent actions in real-time."""
|
|
|
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
"""Initialize activity feed."""
|
|
super().__init__(*args, **kwargs)
|
|
self._content_widget: Static | None = None
|
|
self._activities: list[dict[str, Any]] = []
|
|
|
|
def compose(self) -> Iterator[Static]:
|
|
"""Create the feed content area."""
|
|
content = Static("", id="activity_content")
|
|
self._content_widget = content
|
|
yield content
|
|
|
|
def add_message(
|
|
self, message: str, message_type: str = "info", timestamp: datetime | None = None
|
|
) -> None:
|
|
"""Add a simple text message to the feed."""
|
|
if timestamp is None:
|
|
timestamp = datetime.now()
|
|
|
|
activity = {
|
|
"type": "message",
|
|
"message": message,
|
|
"message_type": message_type,
|
|
"timestamp": timestamp,
|
|
}
|
|
self._activities.append(activity)
|
|
self._render_activities()
|
|
|
|
def add_tool_execution(
|
|
self,
|
|
tool_name: str,
|
|
args: dict[str, Any],
|
|
status: str = "running",
|
|
result: Any = None,
|
|
timestamp: datetime | None = None,
|
|
) -> None:
|
|
"""Add a tool execution block to the feed."""
|
|
if timestamp is None:
|
|
timestamp = datetime.now()
|
|
|
|
activity = {
|
|
"type": "tool",
|
|
"tool_name": tool_name,
|
|
"args": args,
|
|
"status": status,
|
|
"result": result,
|
|
"timestamp": timestamp,
|
|
}
|
|
self._activities.append(activity)
|
|
self._render_activities()
|
|
|
|
def update_last_tool_status(self, status: str, result: Any = None) -> None:
|
|
"""Update the status of the most recent tool execution."""
|
|
# Find the last tool activity
|
|
for activity in reversed(self._activities):
|
|
if activity["type"] == "tool":
|
|
activity["status"] = status
|
|
if result is not None:
|
|
activity["result"] = result
|
|
break
|
|
|
|
self._render_activities()
|
|
|
|
def clear(self) -> None:
|
|
"""Clear all activities from the feed."""
|
|
self._activities.clear()
|
|
if self._content_widget:
|
|
self._content_widget.update("")
|
|
|
|
def _render_activities(self) -> None:
|
|
"""Render all activities to the content widget."""
|
|
if not self._content_widget:
|
|
return
|
|
|
|
if not self._activities:
|
|
placeholder = (
|
|
"\n\n[dim italic]Waiting for agent to start...[/]\n\n"
|
|
"[dim]The activity feed will show real-time updates here.[/]"
|
|
)
|
|
self._content_widget.update(placeholder)
|
|
self._content_widget.set_classes("placeholder")
|
|
return
|
|
|
|
lines = []
|
|
for activity in self._activities:
|
|
if activity["type"] == "message":
|
|
lines.append(self._render_message(activity))
|
|
elif activity["type"] == "tool":
|
|
lines.append(self._render_tool(activity))
|
|
|
|
content = "\n\n".join(lines)
|
|
self._content_widget.update(content)
|
|
self._content_widget.remove_class("placeholder")
|
|
|
|
# Auto-scroll to bottom
|
|
self.call_later(self.scroll_end, animate=False)
|
|
|
|
def _render_message(self, activity: dict[str, Any]) -> str:
|
|
"""Render a simple message activity."""
|
|
timestamp = activity["timestamp"].strftime("%H:%M:%S")
|
|
message = escape_markup(activity["message"])
|
|
message_type = activity["message_type"]
|
|
|
|
# Type-specific styling
|
|
if message_type == "success":
|
|
icon = "[#10b981]✓[/]"
|
|
style = "activity-status-success"
|
|
elif message_type == "error":
|
|
icon = "[#ef4444]✗[/]"
|
|
style = "activity-status-error"
|
|
elif message_type == "warning":
|
|
icon = "[#f59e0b]⚠[/]"
|
|
style = ""
|
|
else:
|
|
icon = "[#6366f1]●[/]"
|
|
style = "activity-status-active"
|
|
|
|
return f"[dim]{timestamp}[/] {icon} [{style}]{message}[/]" if style else f"{message}"
|
|
|
|
def _render_tool(self, activity: dict[str, Any]) -> str:
|
|
"""Render a tool execution block."""
|
|
timestamp = activity["timestamp"].strftime("%H:%M:%S")
|
|
tool_name = activity["tool_name"]
|
|
args = activity["args"]
|
|
status = activity["status"]
|
|
result = activity["result"]
|
|
|
|
# Determine tool type for styling
|
|
tool_class = self._get_tool_class(tool_name)
|
|
|
|
# Status indicator
|
|
if status == "running":
|
|
status_icon = "[#f59e0b]●[/] In progress..."
|
|
elif status == "completed":
|
|
status_icon = "[#10b981]✓[/] Done"
|
|
elif status == "failed":
|
|
status_icon = "[#ef4444]✗[/] Failed"
|
|
else:
|
|
status_icon = "[dim]○[/] Unknown"
|
|
|
|
# Build the tool block
|
|
lines = []
|
|
lines.append(f"[dim]{timestamp}[/]")
|
|
|
|
# Tool header
|
|
header_class = "tool-header" if tool_class else ""
|
|
lines.append(f"[{header_class}]▍ {escape_markup(tool_name)}[/] {status_icon}")
|
|
|
|
# Arguments
|
|
if args:
|
|
for key, value in list(args.items())[:3]: # Show first 3 args
|
|
value_str = str(value)
|
|
if len(value_str) > 100:
|
|
value_str = value_str[:97] + "..."
|
|
|
|
# Special formatting for command
|
|
if key == "command" and tool_name == "terminal_execute":
|
|
lines.append(f" [#10b981]$ {escape_markup(value_str)}[/]")
|
|
else:
|
|
lines.append(f" [dim]{key}:[/] {escape_markup(value_str)}")
|
|
|
|
# Result (if completed)
|
|
if status in ("completed", "failed") and result:
|
|
result_str = str(result)
|
|
if len(result_str) > 200:
|
|
result_str = result_str[:197] + "..."
|
|
|
|
lines.append(f" [dim]→[/] {escape_markup(result_str)}")
|
|
|
|
content = "\n".join(lines)
|
|
|
|
# Wrap in styled container
|
|
return f"[{tool_class}]{content}[/]"
|
|
|
|
def _get_tool_class(self, tool_name: str) -> str:
|
|
"""Get the CSS class for a tool type."""
|
|
tool_classes = {
|
|
"terminal_execute": "tool-terminal",
|
|
"thinking": "tool-thinking",
|
|
"result": "tool-result",
|
|
"error": "tool-error",
|
|
}
|
|
return tool_classes.get(tool_name, "tool-block")
|