diff --git a/Program.cs b/Program.cs index 5a8d1d2..42b585a 100644 --- a/Program.cs +++ b/Program.cs @@ -239,23 +239,70 @@ while (true) } // Phase 1: Show BouncingBar spinner while agent thinks & invokes tools - await AnsiConsole.Status() - .Spinner(Spinner.Known.BouncingBar) - .SpinnerStyle(Style.Parse("cornflowerblue")) - .StartAsync("Thinking...", async ctx => + using var spinnerCts = CancellationTokenSource.CreateLinkedTokenSource(responseCts.Token); + bool showSpinner = true; + + CommandTool.PauseSpinner = () => + { + showSpinner = false; + Console.Write("\r" + new string(' ', 40) + "\r"); + }; + CommandTool.ResumeSpinner = () => + { + showSpinner = true; + }; + + var spinnerTask = Task.Run(async () => + { + var frames = Spinner.Known.BouncingBar.Frames; + var interval = Spinner.Known.BouncingBar.Interval; + int i = 0; + + // Hide cursor + Console.Write("\x1b[?25l"); + try { - while (await stream.MoveNextAsync()) + while (!spinnerCts.Token.IsCancellationRequested) { - responseCts.Token.ThrowIfCancellationRequested(); - CaptureUsage(stream.Current); - if (!string.IsNullOrEmpty(stream.Current.Text)) + if (showSpinner) { - firstChunk = stream.Current.Text; - fullResponse = firstChunk; - return; // Break out → stops the spinner + 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 + { + // Clear the spinner line and show cursor + if (showSpinner) + Console.Write("\r" + new string(' ', 40) + "\r"); + Console.Write("\x1b[?25h"); + } + }); + + try + { + while (await stream.MoveNextAsync()) + { + responseCts.Token.ThrowIfCancellationRequested(); + CaptureUsage(stream.Current); + if (!string.IsNullOrEmpty(stream.Current.Text)) + { + firstChunk = stream.Current.Text; + fullResponse = firstChunk; + break; + } + } + } + finally + { + spinnerCts.Cancel(); + await Task.WhenAny(spinnerTask); + CommandTool.PauseSpinner = null; + CommandTool.ResumeSpinner = null; + } // Phase 2: Stream text tokens directly to the console if (firstChunk != null) diff --git a/Tools/CommandTool.cs b/Tools/CommandTool.cs index 87d9110..e0283af 100644 --- a/Tools/CommandTool.cs +++ b/Tools/CommandTool.cs @@ -10,6 +10,8 @@ namespace AnchorCli.Tools; internal static class CommandTool { public static Action Log { get; set; } = Console.WriteLine; + public static Action? PauseSpinner { get; set; } + public static Action? ResumeSpinner { get; set; } #if WINDOWS [Description("Execute a PowerShell command after user approval. Prompts with [Y/n] before running. Note: For file editing and operations, use the built-in file tools (ReadFile, ReplaceLines, InsertAfter, DeleteRange, CreateFile, etc.) instead of shell commands.")] @@ -22,22 +24,34 @@ internal static class CommandTool Log($"Command request: {command}"); // Prompt for user approval - AnsiConsole.WriteLine(); - AnsiConsole.Write( - new Panel($"[yellow]{Markup.Escape(command)}[/]") - .Header("[bold yellow] Run command? [/]") - .BorderColor(Color.Yellow) - .RoundedBorder() - .Padding(1, 0)); - - // Drain any buffered keystrokes so stale input doesn't auto-answer - while (Console.KeyAvailable) - Console.ReadKey(intercept: true); - - if (!AnsiConsole.Confirm("Execute?", defaultValue: true)) + PauseSpinner?.Invoke(); + try { - AnsiConsole.MarkupLine("[dim grey] ✗ Cancelled by user[/]"); - return "ERROR: Command execution cancelled by user"; + AnsiConsole.WriteLine(); + AnsiConsole.Write( + new Panel($"[yellow]{Markup.Escape(command)}[/]") + .Header("[bold yellow] Run command? [/]") + .BorderColor(Color.Yellow) + .RoundedBorder() + .Padding(1, 0)); + + // Drain any buffered keystrokes so stale input doesn't auto-answer + try + { + while (Console.KeyAvailable) + Console.ReadKey(intercept: true); + } + catch { } + + if (!AnsiConsole.Confirm("Execute?", defaultValue: true)) + { + AnsiConsole.MarkupLine("[dim grey] ✗ Cancelled by user[/]"); + return "ERROR: Command execution cancelled by user"; + } + } + finally + { + ResumeSpinner?.Invoke(); } // Execute the command