项目文件夹

文件
2025-12-13 01:46:54 +08:00

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