Loading
loading
Imported skill loading from langchain
SKILL.md
Full skill instructions
"""Loading widget with animated spinner for agent activity."""
from future import annotations
from time import time from typing import TYPE_CHECKING, ClassVar
from textual.containers import Horizontal from textual.widgets import Static
if TYPE_CHECKING: from textual.app import ComposeResult
class BrailleSpinner: """Animated braille spinner."""
FRAMES: ClassVar[tuple[str, ...]] = (
"⠋",
"⠙",
"⠹",
"⠸",
"⠼",
"⠴",
"⠦",
"⠧",
"⠇",
"⠏",
)
def __init__(self) -> None:
"""Initialize spinner."""
self._position = 0
def next_frame(self) -> str:
"""Get next animation frame."""
frame = self.FRAMES[self._position]
self._position = (self._position + 1) % len(self.FRAMES)
return frame
def current_frame(self) -> str:
"""Get current frame without advancing."""
return self.FRAMES[self._position]
class LoadingWidget(Static): """Animated loading indicator with status text and elapsed time.
Displays: ⠋ Thinking... (3s, esc to interrupt)
"""
DEFAULT_CSS = """
LoadingWidget {
height: auto;
padding: 0 1;
}
LoadingWidget .loading-container {
height: auto;
width: 100%;
}
LoadingWidget .loading-spinner {
width: auto;
color: $warning;
}
LoadingWidget .loading-status {
width: auto;
color: $warning;
}
LoadingWidget .loading-hint {
width: auto;
color: $text-muted;
margin-left: 1;
}
"""
def __init__(self, status: str = "Thinking") -> None:
"""Initialize loading widget.
Args:
status: Initial status text to display
"""
super().__init__()
self._status = status
self._spinner = BrailleSpinner()
self._start_time: float | None = None
self._spinner_widget: Static | None = None
self._status_widget: Static | None = None
self._hint_widget: Static | None = None
self._paused = False
self._paused_elapsed: int = 0
def compose(self) -> ComposeResult:
"""Compose the loading widget layout."""
with Horizontal(classes="loading-container"):
self._spinner_widget = Static(self._spinner.current_frame(), classes="loading-spinner")
yield self._spinner_widget
self._status_widget = Static(f" {self._status}... ", classes="loading-status")
yield self._status_widget
self._hint_widget = Static("(0s, esc to interrupt)", classes="loading-hint")
yield self._hint_widget
def on_mount(self) -> None:
"""Start animation on mount."""
self._start_time = time()
self.set_interval(0.1, self._update_animation)
def _update_animation(self) -> None:
"""Update spinner and elapsed time."""
if self._paused:
return
if self._spinner_widget:
frame = self._spinner.next_frame()
self._spinner_widget.update(f"[#FFD800]{frame}[/]")
if self._hint_widget and self._start_time is not None:
elapsed = int(time() - self._start_time)
self._hint_widget.update(f"({elapsed}s, esc to interrupt)")
def set_status(self, status: str) -> None:
"""Update the status text.
Args:
status: New status text
"""
self._status = status
if self._status_widget:
self._status_widget.update(f" {self._status}... ")
def pause(self, status: str = "Awaiting decision") -> None:
"""Pause the animation and update status.
Args:
status: Status to show while paused
"""
self._paused = True
if self._start_time is not None:
self._paused_elapsed = int(time() - self._start_time)
self._status = status
if self._status_widget:
self._status_widget.update(f" {status}... ")
if self._hint_widget:
self._hint_widget.update(f"(paused at {self._paused_elapsed}s)")
if self._spinner_widget:
self._spinner_widget.update("[dim]⏸[/dim]")
def resume(self) -> None:
"""Resume the animation."""
self._paused = False
self._status = "Thinking"
if self._status_widget:
self._status_widget.update(f" {self._status}... ")
def stop(self) -> None:
"""Stop the animation (widget will be removed by caller)."""
