using System.Text; using Spectre.Console; using AnchorCli.Commands; using AnchorCli.Tools; using AnchorCli.OpenRouter; namespace AnchorCli; /// /// Manages the interactive REPL (Read-Eval-Print Loop) for user interaction. /// Orchestrates input handling, command dispatching, and response display. /// internal sealed class ReplLoop { private readonly ChatSession _session; private readonly TokenTracker _tokenTracker; private readonly CommandDispatcher _commandDispatcher; private readonly SessionManager _sessionManager; private readonly ResponseStreamer _streamer; private readonly UsageDisplayer _usageDisplayer; private readonly ContextCompactionService _compactionService; public ReplLoop( ChatSession session, TokenTracker tokenTracker, CommandDispatcher commandDispatcher, SessionManager sessionManager) { _session = session; _tokenTracker = tokenTracker; _commandDispatcher = commandDispatcher; _sessionManager = sessionManager; _streamer = new ResponseStreamer(session); _usageDisplayer = new UsageDisplayer(tokenTracker); _compactionService = new ContextCompactionService(session.Compactor, session.History, tokenTracker); } public async Task RunAsync() { AnsiConsole.MarkupLine("[dim]Press [bold]Ctrl+C[/] to cancel the current response.[/]"); AnsiConsole.WriteLine(); CancellationTokenSource? responseCts = null; Console.CancelKeyPress += (_, e) => { e.Cancel = true; responseCts?.Cancel(); }; while (true) { AnsiConsole.Markup("[grey]❯ [/]"); string input = InputProcessor.ReadLine("Type your message, or use [bold]/help[/] to see commands."); if (string.IsNullOrWhiteSpace(input)) continue; if (await _commandDispatcher.TryExecuteAsync(input, default)) continue; _session.History.Add(new Microsoft.Extensions.AI.ChatMessage(Microsoft.Extensions.AI.ChatRole.User, input)); AnsiConsole.WriteLine(); responseCts?.Dispose(); responseCts = new CancellationTokenSource(); try { await ProcessTurnAsync(responseCts.Token); } catch (OperationCanceledException) { HandleCancellation(); } catch (Exception ex) { DisplayError(ex); } finally { responseCts?.Dispose(); responseCts = null; } } } private async Task ProcessTurnAsync(CancellationToken cancellationToken) { using var spinner = new SpinnerService(); spinner.Start(cancellationToken); // Configure tool callbacks for spinner control and stale result compaction var originalPause = CommandTool.PauseSpinner; var originalResume = CommandTool.ResumeSpinner; var originalOnFileRead = FileTools.OnFileRead; CommandTool.PauseSpinner = spinner.Pause; CommandTool.ResumeSpinner = spinner.Resume; FileTools.OnFileRead = _ => { int n = ContextCompactor.CompactStaleToolResults(_session.History); if (n > 0) AnsiConsole.MarkupLine($"[dim grey] ♻ Compacted {n} stale tool result(s)[/]"); }; var responseBuilder = new StringBuilder(); bool firstChunkDisplayed = false; try { await foreach (var chunk in _streamer.StreamAsync(cancellationToken)) { // Stop spinner before displaying first chunk if (!firstChunkDisplayed) { await spinner.StopAsync(); firstChunkDisplayed = true; } AnsiConsole.Markup(Markup.Escape(chunk)); responseBuilder.Append(chunk); } } finally { if (!firstChunkDisplayed) { await spinner.StopAsync(); } CommandTool.PauseSpinner = originalPause; CommandTool.ResumeSpinner = originalResume; FileTools.OnFileRead = originalOnFileRead; } var fullResponse = responseBuilder.ToString(); // Display usage statistics _usageDisplayer.Display(_streamer.LastInputTokens, _streamer.LastOutputTokens); _usageDisplayer.DisplaySeparator(); // Add response to history _session.History.Add(new Microsoft.Extensions.AI.ChatMessage(Microsoft.Extensions.AI.ChatRole.Assistant, fullResponse)); // Check for context compaction await _compactionService.TryCompactAsync(); // Save session after turn completes await _sessionManager.SaveAfterTurnAsync(); } private void HandleCancellation() { AnsiConsole.WriteLine(); AnsiConsole.MarkupLine("[dim grey] ✗ Cancelled[/]"); AnsiConsole.Write(new Rule().RuleStyle(Style.Parse("grey dim"))); AnsiConsole.WriteLine(); _session.History.Add(new Microsoft.Extensions.AI.ChatMessage(Microsoft.Extensions.AI.ChatRole.User, "[Response cancelled by user. Acknowledge briefly and wait for the next instruction. Do not repeat what was already said.]")); } private void DisplayError(Exception ex) { AnsiConsole.WriteLine(); AnsiConsole.Write( new Panel($"[red]{Markup.Escape(ex.Message)}[/]") .Header("[bold red] Error [/]") .BorderColor(Color.Red) .RoundedBorder() .Padding(1, 0)); AnsiConsole.WriteLine(); } }