Files
deepagent/deepagents_sourcecode/libs/deepagents-cli/deepagents_cli/widgets/loading.py
HyunjunJeon af5fbfabec 문서 추가: Context Engineering 문서 추가 및 deepagents_sourcecode 한국어 번역
- Context_Engineering.md: 에이전트 컨텍스트 엔지니어링 개념 정리 문서 추가
- Context_Engineering_Research.ipynb: 연구 노트북 업데이트
- deepagents_sourcecode/: docstring과 주석을 한국어로 번역
2026-01-11 17:55:52 +09:00

165 lines
4.5 KiB
Python

"""에이전트 동작 중 표시하는 로딩(스피너) 위젯입니다.
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)."""