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;
}
}