feat: Add CommandTool for user-approved shell command execution, integrating it with a custom spinner that can be paused during interaction.
This commit is contained in:
59
Program.cs
59
Program.cs
@@ -239,10 +239,50 @@ while (true)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Phase 1: Show BouncingBar spinner while agent thinks & invokes tools
|
// Phase 1: Show BouncingBar spinner while agent thinks & invokes tools
|
||||||
await AnsiConsole.Status()
|
using var spinnerCts = CancellationTokenSource.CreateLinkedTokenSource(responseCts.Token);
|
||||||
.Spinner(Spinner.Known.BouncingBar)
|
bool showSpinner = true;
|
||||||
.SpinnerStyle(Style.Parse("cornflowerblue"))
|
|
||||||
.StartAsync("Thinking...", async ctx =>
|
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 (!spinnerCts.Token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if (showSpinner)
|
||||||
|
{
|
||||||
|
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())
|
while (await stream.MoveNextAsync())
|
||||||
{
|
{
|
||||||
@@ -252,10 +292,17 @@ while (true)
|
|||||||
{
|
{
|
||||||
firstChunk = stream.Current.Text;
|
firstChunk = stream.Current.Text;
|
||||||
fullResponse = firstChunk;
|
fullResponse = firstChunk;
|
||||||
return; // Break out → stops the spinner
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
spinnerCts.Cancel();
|
||||||
|
await Task.WhenAny(spinnerTask);
|
||||||
|
CommandTool.PauseSpinner = null;
|
||||||
|
CommandTool.ResumeSpinner = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 2: Stream text tokens directly to the console
|
// Phase 2: Stream text tokens directly to the console
|
||||||
if (firstChunk != null)
|
if (firstChunk != null)
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ namespace AnchorCli.Tools;
|
|||||||
internal static class CommandTool
|
internal static class CommandTool
|
||||||
{
|
{
|
||||||
public static Action<string> Log { get; set; } = Console.WriteLine;
|
public static Action<string> Log { get; set; } = Console.WriteLine;
|
||||||
|
public static Action? PauseSpinner { get; set; }
|
||||||
|
public static Action? ResumeSpinner { get; set; }
|
||||||
|
|
||||||
#if WINDOWS
|
#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.")]
|
[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,6 +24,9 @@ internal static class CommandTool
|
|||||||
Log($"Command request: {command}");
|
Log($"Command request: {command}");
|
||||||
|
|
||||||
// Prompt for user approval
|
// Prompt for user approval
|
||||||
|
PauseSpinner?.Invoke();
|
||||||
|
try
|
||||||
|
{
|
||||||
AnsiConsole.WriteLine();
|
AnsiConsole.WriteLine();
|
||||||
AnsiConsole.Write(
|
AnsiConsole.Write(
|
||||||
new Panel($"[yellow]{Markup.Escape(command)}[/]")
|
new Panel($"[yellow]{Markup.Escape(command)}[/]")
|
||||||
@@ -31,14 +36,23 @@ internal static class CommandTool
|
|||||||
.Padding(1, 0));
|
.Padding(1, 0));
|
||||||
|
|
||||||
// Drain any buffered keystrokes so stale input doesn't auto-answer
|
// Drain any buffered keystrokes so stale input doesn't auto-answer
|
||||||
|
try
|
||||||
|
{
|
||||||
while (Console.KeyAvailable)
|
while (Console.KeyAvailable)
|
||||||
Console.ReadKey(intercept: true);
|
Console.ReadKey(intercept: true);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
if (!AnsiConsole.Confirm("Execute?", defaultValue: true))
|
if (!AnsiConsole.Confirm("Execute?", defaultValue: true))
|
||||||
{
|
{
|
||||||
AnsiConsole.MarkupLine("[dim grey] ✗ Cancelled by user[/]");
|
AnsiConsole.MarkupLine("[dim grey] ✗ Cancelled by user[/]");
|
||||||
return "ERROR: Command execution cancelled by user";
|
return "ERROR: Command execution cancelled by user";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ResumeSpinner?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
// Execute the command
|
// Execute the command
|
||||||
try
|
try
|
||||||
|
|||||||
Reference in New Issue
Block a user