using Spectre.Console; namespace AnchorCli; /// /// Manages the "thinking" spinner animation during AI response generation. /// internal sealed class SpinnerService : IDisposable { private readonly object _consoleLock = new(); private CancellationTokenSource? _spinnerCts; private Task? _spinnerTask; private bool _showSpinner = true; private bool _disposed; /// /// Starts the spinner animation. /// public void Start(CancellationToken cancellationToken) { _spinnerCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); _showSpinner = true; _spinnerTask = Task.Run(async () => { var frames = Spinner.Known.BouncingBar.Frames; var interval = Spinner.Known.BouncingBar.Interval; int i = 0; Console.Write("\x1b[?25l"); try { while (!_spinnerCts.Token.IsCancellationRequested) { lock (_consoleLock) { if (_showSpinner && !_spinnerCts.Token.IsCancellationRequested) { var frame = frames[i % frames.Count]; Console.Write($"\r\x1b[38;5;69m{frame}\x1b[0m Thinking..."); i++; } } try { await Task.Delay(interval, _spinnerCts.Token); } catch { } } } finally { lock (_consoleLock) { if (_showSpinner) Console.Write("\r" + new string(' ', 40) + "\r"); Console.Write("\x1b[?25h"); } } }); } /// /// Stops the spinner animation and waits for it to complete. /// public async Task StopAsync() { _spinnerCts?.Cancel(); if (_spinnerTask != null) { await Task.WhenAny(_spinnerTask); } } /// /// Pauses the spinner (e.g., during tool execution). /// public void Pause() { lock (_consoleLock) { _showSpinner = false; Console.Write("\r" + new string(' ', 40) + "\r"); } } /// /// Resumes the spinner after being paused. /// public void Resume() { lock (_consoleLock) { _showSpinner = true; } } public void Dispose() { if (_disposed) return; _spinnerCts?.Dispose(); _disposed = true; } }