# Loading

> Imported skill loading from langchain

- Skill: `bitwikiorg/loading` (Agent Skill)
- Install (CLI): `npx skillmds@latest add bitwikiorg/loading`
- Raw SKILL.md: https://api.skillmd.com/api/skills/bitwikiorg/loading/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: bitwikiorg (https://skillmd.com/u/bitwikiorg)
- Updated: 2026-08-19
- Page: https://skillmd.com/skills/bitwikiorg/loading

---


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

