Files
westey bad05a2bdc Python: Harness console for python (#6312)
* Add initial harness console for python

* Add textual to project

* Add planning and approval flows with list selector

* Address PR comments

* Fix list selection bug

* Fix PR #6312 round 2 review comments

- Escape untrusted agent text with rich.markup.escape() in observers
  (text_output, planning_output, reasoning_display) to prevent markup injection
- Remove non-functional 'Always approve' choices from tool_approval.py
  (framework lacks CreateAlwaysApproveToolResponse support)
- Remove textual from root pyproject.toml dev deps (sample-specific)
- Add PEP 723 inline script metadata to harness_research.py
- Narrow except Exception to except NoMatches in list_selection.py

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix build error

* Fix build errors

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-09 05:48:35 +00:00

67 lines
2.2 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
"""Agent status widget with spinner animation and usage statistics."""
from __future__ import annotations
from textual.reactive import reactive
from textual.widgets import Static
class AgentStatus(Static):
"""Agent status bar with animated spinner and token usage display.
Displays an animated braille pattern spinner when the agent is active,
along with token usage statistics. The component automatically updates
the spinner animation at ~10fps for smooth visual feedback.
Attributes:
show_spinner: Whether to display the animated spinner.
usage_text: Token usage text to display (e.g., "1.2K in / 856 out").
"""
# Braille pattern spinner frames for smooth animation
SPINNER_FRAMES = ["", "", "", "", "", "", "", "", "", ""]
show_spinner: reactive[bool] = reactive(False)
usage_text: reactive[str] = reactive("")
def __init__(self, **kwargs) -> None:
"""Initialize the agent status widget."""
super().__init__(**kwargs)
self._spinner_index = 0
def on_mount(self) -> None:
"""Start the spinner animation timer when the widget is mounted."""
# Update spinner at ~10fps (every 0.1 seconds)
self.set_interval(0.1, self._advance_spinner)
def _advance_spinner(self) -> None:
"""Advance the spinner to the next frame."""
if self.show_spinner:
self._spinner_index = (self._spinner_index + 1) % len(self.SPINNER_FRAMES)
self.refresh()
def render(self) -> str:
"""Render the status bar with spinner and usage text.
Returns:
Formatted string with Rich markup for spinner and usage display.
"""
if not self.show_spinner and not self.usage_text:
return ""
parts = []
if self.show_spinner:
frame = self.SPINNER_FRAMES[self._spinner_index]
parts.append(f"[cyan]{frame}[/cyan]")
else:
# Keep consistent spacing when spinner is off
parts.append(" ")
if self.usage_text:
parts.append(f"[dim]{self.usage_text}[/dim]")
return " ".join(parts)