feat: modernize TUI status bar with sweep animation

- Replace braille spinner with ping-pong sweep animation using colored squares
- Add smooth gradient fade with 8 color steps from dim to bright green
- Modernize keymap styling: keys in white, actions in dim, separated by ·
- Move "esc stop" to left side next to animation
- Change ctrl-c to ctrl-q for quit
- Simplify "Initializing Agent" to just "Initializing"
- Remove italic styling from status text
- Waiting state shows only "Send message to resume" hint
- Remove unused action verbs and related dead code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
0xallam
2026-01-11 23:54:24 -08:00
parent 9bcb43e713
commit 4818a854d6
2 changed files with 76 additions and 87 deletions

View File

@@ -213,7 +213,7 @@ VulnerabilityDetailScreen {
color: #a3a3a3; color: #a3a3a3;
text-align: left; text-align: left;
content-align: left middle; content-align: left middle;
text-style: italic; text-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }

View File

@@ -2,7 +2,6 @@ import argparse
import asyncio import asyncio
import atexit import atexit
import logging import logging
import random
import signal import signal
import sys import sys
import threading import threading
@@ -18,7 +17,6 @@ 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
@@ -697,25 +695,18 @@ class StrixTUIApp(App): # type: ignore[misc]
self._scan_stop_event = threading.Event() self._scan_stop_event = threading.Event()
self._scan_completed = threading.Event() self._scan_completed = threading.Event()
self._action_verbs = [ self._spinner_frame_index: int = 0 # Current animation frame index
"Generating", self._sweep_num_squares: int = 6 # Number of squares in sweep animation
"Scanning", self._sweep_colors: list[str] = [
"Analyzing", "#000000", # Dimmest (shows dot)
"Hacking", "#031a09",
"Testing", "#052e16",
"Exploiting", "#0d4a2a",
"Pwning", "#15803d",
"Loading", "#22c55e",
"Running", "#4ade80",
"Working", "#86efac", # Brightest
"Strixing",
"Thinking",
"Reasoning",
] ]
self._agent_verbs: dict[str, str] = {} # agent_id -> current_verb
self._agent_verb_timers: dict[str, Any] = {} # agent_id -> timer
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._setup_cleanup_handlers() self._setup_cleanup_handlers()
@@ -927,13 +918,6 @@ class StrixTUIApp(App): # type: ignore[misc]
vuln_indicator = f" ({vuln_count})" if vuln_count > 0 else "" vuln_indicator = f" ({vuln_count})" if vuln_count > 0 else ""
agent_name = f"{status_icon} {agent_name_raw}{vuln_indicator}" agent_name = f"{status_icon} {agent_name_raw}{vuln_indicator}"
if status == "running":
self._start_agent_verb_timer(agent_id)
elif status == "waiting":
self._stop_agent_verb_timer(agent_id)
else:
self._stop_agent_verb_timer(agent_id)
if agent_node.label != agent_name: if agent_node.label != agent_name:
agent_node.set_label(agent_name) agent_node.set_label(agent_name)
return True return True
@@ -1123,9 +1107,14 @@ class StrixTUIApp(App): # type: ignore[misc]
) -> tuple[Text | None, Text, bool]: ) -> tuple[Text | None, Text, bool]:
status = agent_data.get("status", "running") status = agent_data.get("status", "running")
def keymap_text(msg: str) -> Text: def keymap_styled(keys: list[tuple[str, str]]) -> Text:
t = Text() t = Text()
t.append(msg, style="dim") for i, (key, action) in enumerate(keys):
if i > 0:
t.append(" · ", style="dim")
t.append(key, style="white")
t.append(" ", style="dim")
t.append(action, style="dim")
return t return t
simple_statuses: dict[str, tuple[str, str]] = { simple_statuses: dict[str, tuple[str, str]] = {
@@ -1135,10 +1124,10 @@ class StrixTUIApp(App): # type: ignore[misc]
} }
if status in simple_statuses: if status in simple_statuses:
msg, km = simple_statuses[status] msg, _ = simple_statuses[status]
text = Text() text = Text()
text.append(msg) text.append(msg)
return (text, keymap_text(km), False) return (text, Text(), False)
if status == "llm_failed": if status == "llm_failed":
error_msg = agent_data.get("error_message", "") error_msg = agent_data.get("error_message", "")
@@ -1148,20 +1137,25 @@ class StrixTUIApp(App): # type: ignore[misc]
else: else:
text.append("LLM request failed", style="red") text.append("LLM request failed", style="red")
self._stop_dot_animation() self._stop_dot_animation()
return (text, keymap_text("Send message to retry"), False) keymap = Text()
keymap.append("Send message to retry", style="dim")
return (text, keymap, False)
if status == "waiting": if status == "waiting":
animated_text = self._get_animated_waiting_text(agent_id) keymap = Text()
return (animated_text, keymap_text("Send message to resume"), True) keymap.append("Send message to resume", style="dim")
return (Text(" "), keymap, False)
if status == "running": if status == "running":
verb = ( if self._agent_has_real_activity(agent_id):
self._get_agent_verb(agent_id) animated_text = Text()
if self._agent_has_real_activity(agent_id) animated_text.append_text(self._get_sweep_animation(self._sweep_colors))
else "Initializing Agent" animated_text.append("esc", style="white")
) animated_text.append(" ", style="dim")
animated_text = self._get_animated_verb_text(agent_id, verb) animated_text.append("stop", style="dim")
return (animated_text, keymap_text("ESC to stop | CTRL-C to quit and save"), True) return (animated_text, keymap_styled([("ctrl-q", "quit")]), True)
animated_text = self._get_animated_verb_text(agent_id, "Initializing")
return (animated_text, keymap_styled([("ctrl-q", "quit")]), True)
return (None, Text(), False) return (None, Text(), False)
@@ -1266,53 +1260,49 @@ class StrixTUIApp(App): # type: ignore[misc]
return name return name
return None return None
def _get_agent_verb(self, agent_id: str) -> str: def _get_sweep_animation(self, color_palette: list[str]) -> Text:
if agent_id not in self._agent_verbs: text = Text()
self._agent_verbs[agent_id] = random.choice(self._action_verbs) # nosec B311 # noqa: S311 num_squares = self._sweep_num_squares
return self._agent_verbs[agent_id] num_colors = len(color_palette)
def _start_agent_verb_timer(self, agent_id: str) -> None: offset = num_colors - 1
if agent_id not in self._agent_verb_timers: max_pos = (num_squares - 1) + offset
self._agent_verb_timers[agent_id] = self.set_interval( total_range = max_pos + offset
30.0, lambda: self._change_agent_action_verb(agent_id) cycle_length = total_range * 2
) frame_in_cycle = self._spinner_frame_index % cycle_length
def _stop_agent_verb_timer(self, agent_id: str) -> None: wave_pos = total_range - abs(total_range - frame_in_cycle)
if agent_id in self._agent_verb_timers: sweep_pos = wave_pos - offset
self._agent_verb_timers[agent_id].stop()
del self._agent_verb_timers[agent_id]
def _change_agent_action_verb(self, agent_id: str) -> None: dot_color = "#0a3d1f"
if agent_id not in self._agent_verbs:
self._agent_verbs[agent_id] = random.choice(self._action_verbs) # nosec B311 # noqa: S311
return
current_verb = self._agent_verbs[agent_id] for i in range(num_squares):
available_verbs = [verb for verb in self._action_verbs if verb != current_verb] dist = abs(i - sweep_pos)
self._agent_verbs[agent_id] = random.choice(available_verbs) # nosec B311 # noqa: S311 color_idx = max(0, num_colors - 1 - dist)
if self.selected_agent_id == agent_id: if color_idx == 0:
self._update_agent_status_display() text.append("·", style=Style(color=dot_color))
else:
color = color_palette[color_idx]
text.append("", style=Style(color=color))
text.append(" ")
return text
def _get_animated_verb_text(self, agent_id: str, verb: str) -> Text: # noqa: ARG002 def _get_animated_verb_text(self, agent_id: str, verb: str) -> Text: # noqa: ARG002
text = Text() text = Text()
spinner_char = self._spinner_frames[self._spinner_frame_index % len(self._spinner_frames)] sweep = self._get_sweep_animation(self._sweep_colors)
text.append(spinner_char, style=Style(color="#22c55e")) text.append_text(sweep)
text.append(" ", style=Style(color="white")) parts = verb.split(" ", 1)
text.append(verb, style=Style(color="white")) text.append(parts[0], style="white")
return text if len(parts) > 1:
text.append(" ", style="dim")
def _get_animated_waiting_text(self, agent_id: str) -> Text: # noqa: ARG002 text.append(parts[1], style="dim")
text = Text()
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:
if self._dot_animation_timer is None: if self._dot_animation_timer is None:
self._dot_animation_timer = self.set_interval(0.05, self._animate_dots) self._dot_animation_timer = self.set_interval(0.06, self._animate_dots)
def _stop_dot_animation(self) -> None: def _stop_dot_animation(self) -> None:
if self._dot_animation_timer is not None: if self._dot_animation_timer is not None:
@@ -1327,9 +1317,12 @@ 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
self._spinner_frame_index = (self._spinner_frame_index + 1) % len( num_colors = len(self._sweep_colors)
self._spinner_frames offset = num_colors - 1
) max_pos = (self._sweep_num_squares - 1) + offset
total_range = max_pos + offset
cycle_length = total_range * 2
self._spinner_frame_index = (self._spinner_frame_index + 1) % cycle_length
self._update_agent_status_display() self._update_agent_status_display()
if not has_active_agents: if not has_active_agents:
@@ -1350,7 +1343,9 @@ class StrixTUIApp(App): # type: ignore[misc]
tool_name = tool_data.get("tool_name", "") tool_name = tool_data.get("tool_name", "")
if tool_name not in initial_tools: if tool_name not in initial_tools:
return True return True
return False
streaming = self.tracer.get_streaming_content(agent_id)
return bool(streaming and streaming.strip())
def _agent_vulnerability_count(self, agent_id: str) -> int: def _agent_vulnerability_count(self, agent_id: str) -> int:
count = 0 count = 0
@@ -1467,9 +1462,6 @@ class StrixTUIApp(App): # type: ignore[misc]
vuln_indicator = f" ({vuln_count})" if vuln_count > 0 else "" vuln_indicator = f" ({vuln_count})" if vuln_count > 0 else ""
agent_name = f"{status_icon} {agent_name_raw}{vuln_indicator}" agent_name = f"{status_icon} {agent_name_raw}{vuln_indicator}"
if status in ["running", "waiting"]:
self._start_agent_verb_timer(agent_id)
try: try:
if parent_id and parent_id in self.agent_nodes: if parent_id and parent_id in self.agent_nodes:
parent_node = self.agent_nodes[parent_id] parent_node = self.agent_nodes[parent_id]
@@ -1876,9 +1868,6 @@ class StrixTUIApp(App): # type: ignore[misc]
logging.exception(f"Failed to stop agent {agent_id}") logging.exception(f"Failed to stop agent {agent_id}")
def action_custom_quit(self) -> None: def action_custom_quit(self) -> None:
for agent_id in list(self._agent_verb_timers.keys()):
self._stop_agent_verb_timer(agent_id)
if self._scan_thread and self._scan_thread.is_alive(): if self._scan_thread and self._scan_thread.is_alive():
self._scan_stop_event.set() self._scan_stop_event.set()