extracted responsibilities from Program.cs (208→46 lines) and ReplLoop.cs (274→174 lines) into focused service classes: HeaderRenderer, SessionManager, ApplicationStartup, ResponseStreamer, SpinnerService, UsageDisplayer, and ContextCompactionService. Each class now has a single, well-defined responsibility, improving testability and maintainability.
101 lines
2.7 KiB
C#
101 lines
2.7 KiB
C#
using Spectre.Console;
|
|
|
|
namespace AnchorCli;
|
|
|
|
/// <summary>
|
|
/// Manages the "thinking" spinner animation during AI response generation.
|
|
/// </summary>
|
|
internal sealed class SpinnerService : IDisposable
|
|
{
|
|
private readonly object _consoleLock = new();
|
|
private CancellationTokenSource? _spinnerCts;
|
|
private Task? _spinnerTask;
|
|
private bool _showSpinner = true;
|
|
private bool _disposed;
|
|
|
|
/// <summary>
|
|
/// Starts the spinner animation.
|
|
/// </summary>
|
|
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");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops the spinner animation and waits for it to complete.
|
|
/// </summary>
|
|
public async Task StopAsync()
|
|
{
|
|
_spinnerCts?.Cancel();
|
|
if (_spinnerTask != null)
|
|
{
|
|
await Task.WhenAny(_spinnerTask);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pauses the spinner (e.g., during tool execution).
|
|
/// </summary>
|
|
public void Pause()
|
|
{
|
|
lock (_consoleLock)
|
|
{
|
|
_showSpinner = false;
|
|
Console.Write("\r" + new string(' ', 40) + "\r");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resumes the spinner after being paused.
|
|
/// </summary>
|
|
public void Resume()
|
|
{
|
|
lock (_consoleLock)
|
|
{
|
|
_showSpinner = true;
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_spinnerCts?.Dispose();
|
|
_disposed = true;
|
|
}
|
|
}
|