feat(tui): enhance spinner animations and update renderer styles

This commit is contained in:
0xallam
2026-01-06 14:10:45 -08:00
committed by Ahmed Allam
parent 6422bfa0b4
commit 16c9b05121
3 changed files with 45 additions and 63 deletions

View File

@@ -13,12 +13,14 @@ class ViewAgentGraphRenderer(BaseToolRenderer):
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"] css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
@classmethod @classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003 def render(cls, tool_data: dict[str, Any]) -> Static:
text = Text() status = tool_data.get("status", "unknown")
text.append("🕸️ ")
text.append("Viewing agents graph", style="bold #fbbf24")
css_classes = cls.get_css_classes("completed") text = Text()
text.append("", style="#a78bfa")
text.append("viewing agents graph", style="dim")
css_classes = cls.get_css_classes(status)
return Static(text, classes=css_classes) return Static(text, classes=css_classes)
@@ -30,22 +32,21 @@ class CreateAgentRenderer(BaseToolRenderer):
@classmethod @classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {}) args = tool_data.get("args", {})
status = tool_data.get("status", "unknown")
task = args.get("task", "") task = args.get("task", "")
name = args.get("name", "Agent") name = args.get("name", "Agent")
text = Text() text = Text()
text.append("🤖 ") text.append("", style="#a78bfa")
text.append(f"Creating {name}", style="bold #fbbf24") text.append("spawning ", style="dim")
text.append(name, style="bold #a78bfa")
if task: if task:
text.append("\n ") text.append("\n ")
text.append(task, style="dim") text.append(task, style="dim")
else:
text.append("\n ")
text.append("Spawning agent...", style="dim")
css_classes = cls.get_css_classes("completed") css_classes = cls.get_css_classes(status)
return Static(text, classes=css_classes) return Static(text, classes=css_classes)
@@ -57,21 +58,23 @@ class SendMessageToAgentRenderer(BaseToolRenderer):
@classmethod @classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {}) args = tool_data.get("args", {})
status = tool_data.get("status", "unknown")
message = args.get("message", "") message = args.get("message", "")
agent_id = args.get("agent_id", "")
text = Text() text = Text()
text.append("💬 ") text.append("", style="#60a5fa")
text.append("Sending message", style="bold #fbbf24") if agent_id:
text.append(f"to {agent_id}", style="dim")
else:
text.append("sending message", style="dim")
if message: if message:
text.append("\n ") text.append("\n ")
text.append(message, style="dim") text.append(message, style="dim")
else:
text.append("\n ")
text.append("Sending...", style="dim")
css_classes = cls.get_css_classes("completed") css_classes = cls.get_css_classes(status)
return Static(text, classes=css_classes) return Static(text, classes=css_classes)
@@ -120,19 +123,17 @@ class WaitForMessageRenderer(BaseToolRenderer):
@classmethod @classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {}) args = tool_data.get("args", {})
status = tool_data.get("status", "unknown")
reason = args.get("reason", "Waiting for messages from other agents or user input") reason = args.get("reason", "")
text = Text() text = Text()
text.append("⏸️ ") text.append("", style="#6b7280")
text.append("Waiting for messages", style="bold #fbbf24") text.append("waiting", style="dim")
if reason: if reason:
text.append("\n ") text.append("\n ")
text.append(reason, style="dim") text.append(reason, style="dim")
else:
text.append("\n ")
text.append("Agent paused until message received...", style="dim")
css_classes = cls.get_css_classes("completed") css_classes = cls.get_css_classes(status)
return Static(text, classes=css_classes) return Static(text, classes=css_classes)

View File

@@ -55,12 +55,13 @@ class SubagentStartInfoRenderer(BaseToolRenderer):
task = str(args.get("task", "")) task = str(args.get("task", ""))
text = Text() text = Text()
text.append("🤖 Spawned subagent ") text.append("", style="#a78bfa")
text.append(name) text.append("subagent ", style="dim")
text.append(name, style="bold #a78bfa")
if task: if task:
text.append("\n Task: ") text.append("\n ")
text.append(task) text.append(task, style="dim")
css_classes = cls.get_css_classes(status) css_classes = cls.get_css_classes(status)
return Static(text, classes=css_classes) return Static(text, classes=css_classes)

View File

@@ -18,6 +18,7 @@ if TYPE_CHECKING:
from rich.align import Align from rich.align import Align
from rich.console import Group from rich.console import Group
from rich.panel import Panel from rich.panel import Panel
from rich.spinner import SPINNERS
from rich.style import Style from rich.style import Style
from rich.text import Text from rich.text import Text
from textual import events, on from textual import events, on
@@ -346,7 +347,8 @@ class StrixTUIApp(App): # type: ignore[misc]
] ]
self._agent_verbs: dict[str, str] = {} # agent_id -> current_verb self._agent_verbs: dict[str, str] = {} # agent_id -> current_verb
self._agent_verb_timers: dict[str, Any] = {} # agent_id -> timer self._agent_verb_timers: dict[str, Any] = {} # agent_id -> timer
self._agent_dot_states: dict[str, float] = {} # agent_id -> shine position self._spinner_frame_index: int = 0 # Current spinner frame index
self._spinner_frames: list[str] = list(SPINNERS["dots"]["frames"]) # Braille spinner frames
self._dot_animation_timer: Any | None = None self._dot_animation_timer: Any | None = None
self._last_streaming_content: dict[str, str] = {} self._last_streaming_content: dict[str, str] = {}
@@ -895,30 +897,20 @@ class StrixTUIApp(App): # type: ignore[misc]
if self.selected_agent_id == agent_id: if self.selected_agent_id == agent_id:
self._update_agent_status_display() self._update_agent_status_display()
def _get_shine_style(self, dist: float) -> Style: def _get_animated_verb_text(self, agent_id: str, verb: str) -> Text: # noqa: ARG002
if dist <= 0.5:
return Style(color="bright_white", bold=True)
if dist <= 1.5:
return Style(color="white", bold=True)
if dist <= 2.5:
return Style(color="#a3a3a3")
return Style(color="#525252")
def _get_animated_verb_text(self, agent_id: str, verb: str) -> Text:
if agent_id not in self._agent_dot_states:
self._agent_dot_states[agent_id] = 0.0
shine_pos = self._agent_dot_states[agent_id]
text = Text() text = Text()
for i, char in enumerate(verb): spinner_char = self._spinner_frames[self._spinner_frame_index % len(self._spinner_frames)]
dist = abs(i - shine_pos) text.append(spinner_char, style=Style(color="#22c55e"))
text.append(char, style=self._get_shine_style(dist)) text.append(" ", style=Style(color="white"))
text.append(verb, style=Style(color="white"))
return text return text
def _get_animated_waiting_text(self, agent_id: str) -> Text: # noqa: ARG002 def _get_animated_waiting_text(self, agent_id: str) -> Text: # noqa: ARG002
text = Text() text = Text()
text.append("Waiting", style="#fbbf24") spinner_char = self._spinner_frames[self._spinner_frame_index % len(self._spinner_frames)]
text.append(spinner_char, style=Style(color="#fbbf24"))
text.append(" ", style=Style(color="white"))
text.append("Waiting", style=Style(color="#fbbf24"))
return text return text
def _start_dot_animation(self) -> None: def _start_dot_animation(self) -> None:
@@ -938,16 +930,8 @@ class StrixTUIApp(App): # type: ignore[misc]
status = agent_data.get("status", "running") status = agent_data.get("status", "running")
if status in ["running", "waiting"]: if status in ["running", "waiting"]:
has_active_agents = True has_active_agents = True
if status == "waiting": self._spinner_frame_index = (self._spinner_frame_index + 1) % len(
verb = "Waiting" self._spinner_frames
elif self._agent_has_real_activity(self.selected_agent_id):
verb = self._get_agent_verb(self.selected_agent_id)
else:
verb = "Initializing Agent"
text_len = len(verb)
current_shine = self._agent_dot_states.get(self.selected_agent_id, 0.0)
self._agent_dot_states[self.selected_agent_id] = (current_shine + 0.5) % (
text_len + 3
) )
self._update_agent_status_display() self._update_agent_status_display()
@@ -959,11 +943,7 @@ class StrixTUIApp(App): # type: ignore[misc]
if not has_active_agents: if not has_active_agents:
self._stop_dot_animation() self._stop_dot_animation()
for agent_id in list(self._agent_dot_states.keys()): self._spinner_frame_index = 0
if agent_id not in self.tracer.agents or self.tracer.agents[agent_id].get(
"status"
) not in ["running", "waiting"]:
del self._agent_dot_states[agent_id]
def _agent_has_real_activity(self, agent_id: str) -> bool: def _agent_has_real_activity(self, agent_id: str) -> bool:
initial_tools = {"scan_start_info", "subagent_start_info"} initial_tools = {"scan_start_info", "subagent_start_info"}