From 2e2fe08dc3c1ac759c3e508108ba9dcab7bc84a5 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 26 Jun 2025 11:02:46 -0700 Subject: [PATCH 1/6] Allow AIShell to run command in the connected PowerShell session and collect all output and error. 1. Add 'Invoke-AICommand' cmdlet (alias: 'airun') to 'AIShell' module. Commands sent from the side-car AIShell will be executed through this command in the form of `airun { }`. This command is designed to collect all output and error messages as they are displayed in the terminal, while preserving the streaming behavior as expected. 2. Add 'RunCommand' and 'PostResult' messages to the protocol. 3. Update the 'Channel' class in 'AIShell' module to support the 'OnRunCommand' action. We already support posting command to the PowerShell's prompt, but it turns out not easy to make the command be accepted. On Windows, we have to call 'AcceptLine' within an 'OnIdle' event handler and it also requires changes to PsReadLine. - 'AcceptLine' only set a flag in PSReadLine to indicate the line was accepted. The flag is checked in 'InputLoop', however, when PSReadLine is waiting for input, it's blocked in the 'ReadKey' call within 'InputLoop', so even if the flag is set, 'InputLoop' won't be able to check it until after 'ReadKey' call is returned. - I need to change PSReadLine a bit: After it finishes handling the 'OnIdle' event, it checks if the '_lineAccepted' flag is set. If it's set, it means 'AcceptLine' got called within the 'OnIdle' handler, and it throws a 'LineAcceptedException' to break out from 'ReadKey'. I catch this exception in 'InputLoop' to continue with the flag check. - However, a problem with this change is: the "readkey thread" is still blocked on 'Console.ReadKey' when the command is returned to PowerShell to execute. On Windows, this could cause minor issues if the command also calls 'Console.ReadKey' -- 2 threads calling 'Console.ReadKey' in parallel, so it's uncertian which will get the next keystroke input. On macOS and Linux, the problem is way much bigger -- any subsequent writing to the terminal may be blocked, because on non-Windows, reading cursor position will be blocked if another thread is calling 'Console.ReadKey'. - So, this approach can only work on Windows. On macOS, we depend on iTerm2, which has a Python API server and it's possible to send keystrokes to a tab using the Python API, so we could use that for macOS. But Windows Terminal doesn't support that, and thus we will have to use the above approach to accept the command on Windows. - On macOS, if the Python API approach works fine, then we could even consider using it for the 'PostCode' action. 4. Add '/code run ' to test out the 'RunCommand' functionality end-to-end. --- shell/AIShell.Abstraction/NamedPipe.cs | 138 +++++++++++++++++- shell/AIShell.Integration/AIShell.psd1 | 4 +- shell/AIShell.Integration/Channel.cs | 136 ++++++++++++++++- .../Commands/InvokeAiCommand.cs | 90 ++++++++++++ shell/AIShell.Integration/RunInTerminal.cs | 40 +++++ shell/AIShell.Kernel/Command/CodeCommand.cs | 16 ++ .../ShellIntegration/Channel.cs | 9 ++ 7 files changed, 421 insertions(+), 12 deletions(-) create mode 100644 shell/AIShell.Integration/Commands/InvokeAiCommand.cs create mode 100644 shell/AIShell.Integration/RunInTerminal.cs diff --git a/shell/AIShell.Abstraction/NamedPipe.cs b/shell/AIShell.Abstraction/NamedPipe.cs index 624b12ed..e5d42aff 100644 --- a/shell/AIShell.Abstraction/NamedPipe.cs +++ b/shell/AIShell.Abstraction/NamedPipe.cs @@ -33,6 +33,16 @@ public enum MessageType : int /// A message from AIShell to command-line shell to send code block. /// PostCode = 4, + + /// + /// A message from AIShell to command-line shell to run a command. + /// + RunCommand = 5, + + /// + /// A message from command-line shell to AIShell to post the result of a command. + /// + PostResult = 6, } /// @@ -201,6 +211,74 @@ public PostCodeMessage(List codeBlocks) } } +/// +/// Message for . +/// +public sealed class RunCommandMessage : PipeMessage +{ + /// + /// Gets the command to run. + /// + public string Command { get; } + + /// + /// Gets whether the command should be run in blocking mode. + /// + public bool Blocking { get; } + + /// + /// Creates an instance of . + /// + public RunCommandMessage(string command, bool blocking) + : base(MessageType.RunCommand) + { + ArgumentException.ThrowIfNullOrEmpty(command); + + Command = command; + Blocking = blocking; + } +} + +/// +/// Message for . +/// +public sealed class PostResultMessage : PipeMessage +{ + /// + /// Gets the result of the command for a blocking 'run_command' too call. + /// Or, for a non-blocking call, gets the id for retrieving the result later. + /// + public string Output { get; } + + /// + /// Gets whether the command execution had any error. + /// i.e. a native command returned a non-zero exit code, or a powershell command threw any errors. + /// + public bool HadError { get; } + + /// + /// Gets a value indicating whether the operation was canceled by the user. + /// + public bool UserCancelled { get; } + + /// + /// Gets the internal exception message that is thrown when trying to run the command. + /// + public string Exception { get; } + + /// + /// Creates an instance of . + /// + public PostResultMessage(string output, bool hadError, bool userCancelled, string exception) + : base(MessageType.PostResult) + { + Output = output; + HadError = hadError; + UserCancelled = userCancelled; + Exception = exception; + } +} + /// /// The base type for common pipe operations. /// @@ -301,7 +379,7 @@ protected async Task GetMessageAsync(CancellationToken cancellation return null; } - if (type > (int)MessageType.PostCode) + if (type > (int)MessageType.PostResult) { _pipeStream.Close(); throw new IOException($"Unknown message type received: {type}. Connection was dropped."); @@ -344,9 +422,11 @@ private static PipeMessage DeserializePayload(int type, ReadOnlySpan bytes { (int)MessageType.PostQuery => JsonSerializer.Deserialize(bytes), (int)MessageType.AskConnection => JsonSerializer.Deserialize(bytes), - (int)MessageType.PostContext => JsonSerializer.Deserialize(bytes), (int)MessageType.AskContext => JsonSerializer.Deserialize(bytes), + (int)MessageType.PostContext => JsonSerializer.Deserialize(bytes), (int)MessageType.PostCode => JsonSerializer.Deserialize(bytes), + (int)MessageType.RunCommand => JsonSerializer.Deserialize(bytes), + (int)MessageType.PostResult => JsonSerializer.Deserialize(bytes), _ => throw new NotSupportedException("Unreachable code"), }; } @@ -465,6 +545,11 @@ public async Task StartProcessingAsync(int timeout, CancellationToken cancellati InvokeOnPostCode((PostCodeMessage)message); break; + case MessageType.RunCommand: + var result = InvokeOnRunCommand((RunCommandMessage)message); + SendMessage(result); + break; + default: // Log: unexpected messages ignored. break; @@ -537,6 +622,33 @@ private PostContextMessage InvokeOnAskContext(AskContextMessage message) return null; } + private PostResultMessage InvokeOnRunCommand(RunCommandMessage message) + { + if (OnRunCommand is null) + { + // Log: event handler not set. + return new PostResultMessage( + output: "Command execution is not supported.", + hadError: true, + userCancelled: false, + exception: null); + } + + try + { + return OnRunCommand(message); + } + catch (Exception e) + { + // Log: exception when invoking 'OnRunCommand' + return new PostResultMessage( + output: "Failed to execute the command due to an internal error.", + hadError: true, + userCancelled: false, + exception: e.Message); + } + } + /// /// Event for handling the message. /// @@ -551,6 +663,11 @@ private PostContextMessage InvokeOnAskContext(AskContextMessage message) /// Event for handling the message. /// public event Func OnAskContext; + + /// + /// Event for handling the message. + /// + public event Func OnRunCommand; } /// @@ -771,4 +888,21 @@ public async Task AskContext(AskContextMessage message, Canc return postContext; } + + public async Task RunCommand(RunCommandMessage message, CancellationToken cancellationToken) + { + // Send the request message to the shell. + SendMessage(message); + + // Receiving response from the shell. + var response = await GetMessageAsync(cancellationToken); + if (response is not PostResultMessage postResult) + { + // Log: unexpected message. drop connection. + _client.Close(); + throw new IOException($"Expecting '{MessageType.PostResult}' response, but received '{message.Type}' message."); + } + + return postResult; + } } diff --git a/shell/AIShell.Integration/AIShell.psd1 b/shell/AIShell.Integration/AIShell.psd1 index 162a7171..3d40b6d6 100644 --- a/shell/AIShell.Integration/AIShell.psd1 +++ b/shell/AIShell.Integration/AIShell.psd1 @@ -10,9 +10,9 @@ PowerShellVersion = '7.4.6' PowerShellHostName = 'ConsoleHost' FunctionsToExport = @() - CmdletsToExport = @('Start-AIShell','Invoke-AIShell','Resolve-Error') + CmdletsToExport = @('Start-AIShell','Invoke-AIShell', 'Invoke-AICommand', 'Resolve-Error') VariablesToExport = '*' - AliasesToExport = @('aish', 'askai', 'fixit') + AliasesToExport = @('aish', 'askai', 'fixit', 'airun') HelpInfoURI = 'https://aka.ms/aishell-help' PrivateData = @{ PSData = @{ Prerelease = 'preview5'; ProjectUri = 'https://github.com/PowerShell/AIShell' } } } diff --git a/shell/AIShell.Integration/Channel.cs b/shell/AIShell.Integration/Channel.cs index 7561167f..a591291f 100644 --- a/shell/AIShell.Integration/Channel.cs +++ b/shell/AIShell.Integration/Channel.cs @@ -25,7 +25,8 @@ public class Channel : IDisposable private readonly object _psrlSingleton; private readonly ManualResetEvent _connSetupWaitHandler; private readonly Predictor _predictor; - private readonly ScriptBlock _onIdleAction; + private readonly ScriptBlock _onIdlePostAction; + private readonly ScriptBlock _onIdleRunAction; private readonly List _commandHistory; private PathInfo _currentLocation; @@ -35,6 +36,8 @@ public class Channel : IDisposable private Exception _exception; private Thread _serverThread; private CodePostData _pendingPostCodeData; + private RunCommandRequest _runCommandRequest; + private PowerShell _pwsh; private Channel(Runspace runspace, EngineIntrinsics intrinsics, Type psConsoleReadLineType) { @@ -70,7 +73,8 @@ private Channel(Runspace runspace, EngineIntrinsics intrinsics, Type psConsoleRe _commandHistory = []; _predictor = new Predictor(); - _onIdleAction = ScriptBlock.Create("[AIShell.Integration.Channel]::Singleton.OnIdleHandler()"); + _onIdlePostAction = ScriptBlock.Create("[AIShell.Integration.Channel]::Singleton.OnIdlePostHandler()"); + _onIdleRunAction = ScriptBlock.Create("[AIShell.Integration.Channel]::Singleton.OnIdleRunHandler()"); } public static Channel CreateSingleton(Runspace runspace, EngineIntrinsics intrinsics, Type psConsoleReadLineType) @@ -106,6 +110,29 @@ internal bool CheckConnection(bool blocking, out bool setupInProgress) return false; } + /// + /// A 'run_command' tool call request will set '_runCommandRequest' properly with the 'Result' property being null. + /// For a blocking call, it will set '_runCommandRequest' back to null once the call result has been set. + /// For an unblocking call, '_runCommandRequest' will remain as is until: + /// 1. a 'get_output' request comes to collect the result, or + /// 2. another 'run_command' request comes to run a new command. + /// So we consider there is a pending request only if '_runCommandRequest' is not null and its 'Result' property is null. + /// + internal string GetRunCommandRequest() => + _runCommandRequest is { Result: null } ? _runCommandRequest.Command : null; + + /// + /// Set the command result for a 'run_command' tool call request. + /// + internal void SetRunCommandResult(bool hadErrors, bool userCancelled, List errorAndOutput) + { + if (_runCommandRequest is { Result: null }) + { + _runCommandRequest.Result = new(hadErrors, userCancelled, errorAndOutput); + _runCommandRequest.Event?.Set(); + } + } + public string StartChannelSetup() { if (_serverPipe is not null) @@ -123,12 +150,13 @@ public string StartChannelSetup() _serverPipe.OnAskConnection += OnAskConnection; _serverPipe.OnAskContext += OnAskContext; _serverPipe.OnPostCode += OnPostCode; + _serverPipe.OnRunCommand += OnRunCommand; _serverThread = new Thread(ThreadProc) - { - IsBackground = true, - Name = "pwsh channel thread" - }; + { + IsBackground = true, + Name = "pwsh channel thread" + }; _serverThread.Start(); return _shellPipeName; @@ -255,6 +283,7 @@ private void Reset() _serverPipe.OnAskConnection -= OnAskConnection; _serverPipe.OnAskContext -= OnAskContext; _serverPipe.OnPostCode -= OnPostCode; + _serverPipe.OnRunCommand -= OnRunCommand; } _serverPipe = null; @@ -284,7 +313,7 @@ private void ThrowIfNotConnected() } [Hidden()] - public void OnIdleHandler() + public void OnIdlePostHandler() { if (_pendingPostCodeData is not null) { @@ -294,6 +323,17 @@ public void OnIdleHandler() } } + [Hidden()] + public void OnIdleRunHandler() + { + if (_pendingPostCodeData is not null) + { + PSRLInsert(_pendingPostCodeData.CodeToInsert); + PSRLAcceptLine(); + _pendingPostCodeData = null; + } + } + private void OnPostCode(PostCodeMessage postCodeMessage) { // Ignore 'code post' request when a posting operation is on-going. @@ -351,7 +391,7 @@ private void OnPostCode(PostCodeMessage postCodeMessage) eventName: null, sourceIdentifier: PSEngineEvent.OnIdle, data: null, - action: _onIdleAction, + action: _onIdlePostAction, supportEvent: true, forwardEvent: false, maxTriggerCount: 1); @@ -448,6 +488,86 @@ private void OnAskConnection(ShellClientPipe clientPipe, Exception exception) _connSetupWaitHandler.Set(); } + private PostContextMessage OnAskContext(AskContextMessage askContextMessage) + { + // Not implemented yet. + return null; + } + + private PostResultMessage OnRunCommand(RunCommandMessage runCommandMessage) + { + // Ignore 'run_command' request when a code posting operation is on-going. + if (_pendingPostCodeData is not null) + { + return new PostResultMessage( + output: "Cannot run command at the moment. Try again later.", + hadError: true, + userCancelled: false, + exception: null); + } + + string command = runCommandMessage.Command.Replace("\r\n", "\n"); + _runCommandRequest = new(command, runCommandMessage.Blocking); + + string codeToInsert = command.Contains('\n') + ? $$""" + airun { + {{command}} + } + """ + : $"airun {{ {command} }}"; + + // When PSReadLine is actively running, its '_readLineReady' field should be set to 'true'. + // When the value is 'false', it means PowerShell is still busy running scripts or commands. + if (_psrlReadLineReady.GetValue(_psrlSingleton) is true) + { + PSRLRevertLine(); + } + + _pendingPostCodeData = new CodePostData(codeToInsert, null); + // We use script block handler instead of a delegate handler because the latter will run + // in a background thread, while the former will run in the pipeline thread, which is way + // more predictable. + _runspace.Events.SubscribeEvent( + source: null, + eventName: null, + sourceIdentifier: PSEngineEvent.OnIdle, + data: null, + action: _onIdleRunAction, + supportEvent: true, + forwardEvent: false, + maxTriggerCount: 1); + + if (runCommandMessage.Blocking) + { + // Wait for the call to finish. + _runCommandRequest.Event.Wait(); + RunCommandResult result = _runCommandRequest.Result; + + _pwsh ??= PowerShell.Create(); + _pwsh.Commands.Clear(); + string output = result.ErrorAndOutput.Count is 0 + ? string.Empty + : _pwsh.AddCommand("Out-String") + .AddParameter("InputObject", result.ErrorAndOutput) + .AddParameter("Width", 120) + .Invoke()[0]; + + PostResultMessage response = new( + output: output, + hadError: result.HadErrors, + userCancelled: result.UserCancelled, + exception: null); + + _runCommandRequest.Dispose(); + _runCommandRequest = null; + + return response; + } + + return new PostResultMessage(output: _runCommandRequest.Id, hadError: false, userCancelled: false, exception: null); + } + private void PSRLInsert(string text) { using var _ = new NoWindowResizingCheck(); diff --git a/shell/AIShell.Integration/Commands/InvokeAiCommand.cs b/shell/AIShell.Integration/Commands/InvokeAiCommand.cs new file mode 100644 index 00000000..c5d2dc90 --- /dev/null +++ b/shell/AIShell.Integration/Commands/InvokeAiCommand.cs @@ -0,0 +1,90 @@ +namespace AIShell.Integration.Commands; + +using System.Management.Automation; + +[Alias("airun")] +[Cmdlet(VerbsLifecycle.Invoke, "AICommand")] +public sealed class InvokeAICommand : PSCmdlet, IDisposable +{ + private readonly PowerShell _pwsh; + private readonly PSDataCollection _output; + + private bool _disposed, _hadErrors, _cancelled; + private List _capturedContent; + + [Parameter(Mandatory = true, Position = 0)] + public ScriptBlock Command { get; set; } + + public InvokeAICommand() + { + _pwsh = PowerShell.Create(RunspaceMode.CurrentRunspace); + _pwsh.Streams.Error.DataAdding += DataAddingHandler; + + _output = []; + _output.DataAdding += DataAddingHandler; + _capturedContent = null; + } + + /// + /// The handler for both 'OutputDataAdding' and 'ErrorDataAdding' events. + /// + /// + /// The handler is called on the pipeline thread, so it's safe to call 'WriteObject' in it. + /// + private void DataAddingHandler(object sender, DataAddingEventArgs e) + { + object item = e.ItemAdded; + _capturedContent?.Add(item); + WriteObject(item); + } + + protected override void EndProcessing() + { + string commandToRun = Command.ToString(); + string requestedCommand = Channel.Singleton.GetRunCommandRequest(); + + if (requestedCommand is not null && commandToRun.Contains(requestedCommand)) + { + // Only capture output when this is a tool call invoked by AI. + _capturedContent = []; + } + + try + { + _pwsh.AddScript(commandToRun, useLocalScope: false); + _pwsh.Invoke(input: null, _output, settings: null); + } + finally + { + _hadErrors = _pwsh.HadErrors; + } + } + + protected override void StopProcessing() + { + _pwsh.Stop(); + _cancelled = true; + } + + /// + /// Dispose the resources. + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + if (_capturedContent is { }) + { + Channel.Singleton.SetRunCommandResult(_hadErrors, _cancelled, _capturedContent); + } + + _output.DataAdding -= DataAddingHandler; + _output.Dispose(); + _pwsh.Streams.Error.DataAdding -= DataAddingHandler; + _pwsh.Dispose(); + _disposed = true; + } +} diff --git a/shell/AIShell.Integration/RunInTerminal.cs b/shell/AIShell.Integration/RunInTerminal.cs new file mode 100644 index 00000000..da9d28d5 --- /dev/null +++ b/shell/AIShell.Integration/RunInTerminal.cs @@ -0,0 +1,40 @@ +namespace AIShell.Integration; + +internal class RunCommandRequest : IDisposable +{ + internal string Id { get; } + internal string Command { get; } + internal ManualResetEventSlim Event { get; } + internal RunCommandResult Result { get; set; } + + internal RunCommandRequest(string command, bool blockingCall) + { + ArgumentException.ThrowIfNullOrEmpty(command); + + Id = Guid.NewGuid().ToString(); + Command = command; + Event = blockingCall ? new() : null; + Result = null; + } + + public void Dispose() + { + Event?.Dispose(); + } +} + +internal class RunCommandResult +{ + internal bool HadErrors { get; } + internal bool UserCancelled { get; } + internal List ErrorAndOutput { get; } + + internal RunCommandResult(bool hadErrors, bool userCancelled, List errorAndOutput) + { + ArgumentNullException.ThrowIfNull(errorAndOutput); + + HadErrors = hadErrors; + UserCancelled = userCancelled; + ErrorAndOutput = errorAndOutput; + } +} diff --git a/shell/AIShell.Kernel/Command/CodeCommand.cs b/shell/AIShell.Kernel/Command/CodeCommand.cs index 133dbe37..db023fdc 100644 --- a/shell/AIShell.Kernel/Command/CodeCommand.cs +++ b/shell/AIShell.Kernel/Command/CodeCommand.cs @@ -36,6 +36,13 @@ public CodeCommand() copy.SetHandler(CopyAction, nth); save.SetHandler(SaveAction, file, append); post.SetHandler(PostAction, nth); + + + var run = new Command("run", "Run the specified command."); + var command = new Argument("command", "command to run"); + run.AddArgument(command); + run.SetHandler(RunAction, command); + AddCommand(run); } private static string GetCodeText(Shell shell, int index) @@ -181,4 +188,13 @@ private void PostAction(int nth) host.WriteErrorLine(e.Message); } } + + private async Task RunAction(string command) + { + var shell = (Shell)Shell; + var host = shell.Host; + var result = await shell.Channel.RunCommand(new RunCommandMessage(command, blocking: true), shell.CancellationToken); + + host.WriteLine($"HadError: {result.HadError}\nUserCancelled: {result.UserCancelled}\nOutput: {result.Output}\nException: {result.Exception}"); + } } diff --git a/shell/AIShell.Kernel/ShellIntegration/Channel.cs b/shell/AIShell.Kernel/ShellIntegration/Channel.cs index dfdc7054..e2311141 100644 --- a/shell/AIShell.Kernel/ShellIntegration/Channel.cs +++ b/shell/AIShell.Kernel/ShellIntegration/Channel.cs @@ -212,6 +212,15 @@ internal async Task AskContext(AskContextMessage message, Ca return await _clientPipe.AskContext(message, cancellationToken); } + /// + /// Run command in the connected shell. + /// + internal async Task RunCommand(RunCommandMessage message, CancellationToken cancellationToken) + { + ThrowIfNotConnected(); + return await _clientPipe.RunCommand(message, cancellationToken); + } + public void Dispose() { if (_disposed) From 671a215340cfb5047ba38eee2b4c6cf64fb10dd2 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 9 Jul 2025 11:44:39 -0700 Subject: [PATCH 2/6] Enable 'run_command_in_terminal' and 'get_command_output' tools --- shell/AIShell.Abstraction/NamedPipe.cs | 103 +++++++++++++++++- shell/AIShell.Integration/Channel.cs | 66 +++++++++-- shell/AIShell.Kernel/Command/CodeCommand.cs | 16 --- shell/AIShell.Kernel/Host.cs | 50 ++++++++- shell/AIShell.Kernel/MCP/BuiltInTool.cs | 95 ++++++++++++---- shell/AIShell.Kernel/MCP/McpTool.cs | 2 +- .../ShellIntegration/Channel.cs | 9 ++ 7 files changed, 287 insertions(+), 54 deletions(-) diff --git a/shell/AIShell.Abstraction/NamedPipe.cs b/shell/AIShell.Abstraction/NamedPipe.cs index e5d42aff..a9da0022 100644 --- a/shell/AIShell.Abstraction/NamedPipe.cs +++ b/shell/AIShell.Abstraction/NamedPipe.cs @@ -39,10 +39,15 @@ public enum MessageType : int /// RunCommand = 5, + /// + /// A message from AIShell to command-line shell to ask for the result of a previous command run. + /// + AskCommandOutput = 6, + /// /// A message from command-line shell to AIShell to post the result of a command. /// - PostResult = 6, + PostResult = 7, } /// @@ -239,6 +244,27 @@ public RunCommandMessage(string command, bool blocking) } } +/// +/// Message for . +/// +public sealed class AskCommandOutputMessage : PipeMessage +{ + /// + /// Gets the id of the command to retrieve the output for. + /// + public string CommandId { get; } + + /// + /// Creates an instance of . + /// + public AskCommandOutputMessage(string commandId) + : base(MessageType.AskCommandOutput) + { + ArgumentException.ThrowIfNullOrEmpty(commandId); + CommandId = commandId; + } +} + /// /// Message for . /// @@ -426,6 +452,7 @@ private static PipeMessage DeserializePayload(int type, ReadOnlySpan bytes (int)MessageType.PostContext => JsonSerializer.Deserialize(bytes), (int)MessageType.PostCode => JsonSerializer.Deserialize(bytes), (int)MessageType.RunCommand => JsonSerializer.Deserialize(bytes), + (int)MessageType.AskCommandOutput => JsonSerializer.Deserialize(bytes), (int)MessageType.PostResult => JsonSerializer.Deserialize(bytes), _ => throw new NotSupportedException("Unreachable code"), }; @@ -550,6 +577,11 @@ public async Task StartProcessingAsync(int timeout, CancellationToken cancellati SendMessage(result); break; + case MessageType.AskCommandOutput: + var output = InvokeOnAskCommandOutput((AskCommandOutputMessage)message); + SendMessage(output); + break; + default: // Log: unexpected messages ignored. break; @@ -622,6 +654,9 @@ private PostContextMessage InvokeOnAskContext(AskContextMessage message) return null; } + /// + /// Helper to invoke the event. + /// private PostResultMessage InvokeOnRunCommand(RunCommandMessage message) { if (OnRunCommand is null) @@ -649,6 +684,36 @@ private PostResultMessage InvokeOnRunCommand(RunCommandMessage message) } } + /// + /// Helper to invoke the event. + /// + private PostResultMessage InvokeOnAskCommandOutput(AskCommandOutputMessage message) + { + if (OnAskCommandOutput is null) + { + // Log: event handler not set. + return new PostResultMessage( + output: "Retrieving command output is not supported.", + hadError: true, + userCancelled: false, + exception: null); + } + + try + { + return OnAskCommandOutput(message); + } + catch (Exception e) + { + // Log: exception when invoking 'OnAskCommandOutput' + return new PostResultMessage( + output: "Failed to retrieve the command output due to an internal error.", + hadError: true, + userCancelled: false, + exception: e.Message); + } + } + /// /// Event for handling the message. /// @@ -668,6 +733,11 @@ private PostResultMessage InvokeOnRunCommand(RunCommandMessage message) /// Event for handling the message. /// public event Func OnRunCommand; + + /// + /// Event for handling the message. + /// + public event Func OnAskCommandOutput; } /// @@ -889,6 +959,13 @@ public async Task AskContext(AskContextMessage message, Canc return postContext; } + /// + /// Run a command in the connected PowerShell session. + /// + /// The message. + /// A cancellation token. + /// A message as the response. + /// Throws when the pipe is closed by the other side. public async Task RunCommand(RunCommandMessage message, CancellationToken cancellationToken) { // Send the request message to the shell. @@ -905,4 +982,28 @@ public async Task RunCommand(RunCommandMessage message, Cance return postResult; } + + /// + /// Ask for the output of a previously run command in the connected PowerShell session. + /// + /// The message. + /// A cancellation token. + /// A message as the response. + /// Throws when the pipe is closed by the other side. + public async Task AskCommandOutput(AskCommandOutputMessage message, CancellationToken cancellationToken) + { + // Send the request message to the shell. + SendMessage(message); + + // Receiving response from the shell. + var response = await GetMessageAsync(cancellationToken); + if (response is not PostResultMessage postResult) + { + // Log: unexpected message. drop connection. + _client.Close(); + throw new IOException($"Expecting '{MessageType.PostResult}' response, but received '{message.Type}' message."); + } + + return postResult; + } } diff --git a/shell/AIShell.Integration/Channel.cs b/shell/AIShell.Integration/Channel.cs index a591291f..1306d563 100644 --- a/shell/AIShell.Integration/Channel.cs +++ b/shell/AIShell.Integration/Channel.cs @@ -151,6 +151,7 @@ public string StartChannelSetup() _serverPipe.OnAskContext += OnAskContext; _serverPipe.OnPostCode += OnPostCode; _serverPipe.OnRunCommand += OnRunCommand; + _serverPipe.OnAskCommandOutput += OnAskCommandOutput; _serverThread = new Thread(ThreadProc) { @@ -284,6 +285,7 @@ private void Reset() _serverPipe.OnAskContext -= OnAskContext; _serverPipe.OnPostCode -= OnPostCode; _serverPipe.OnRunCommand -= OnRunCommand; + _serverPipe.OnAskCommandOutput -= OnAskCommandOutput; } _serverPipe = null; @@ -488,12 +490,6 @@ private void OnAskConnection(ShellClientPipe clientPipe, Exception exception) _connSetupWaitHandler.Set(); } - private PostContextMessage OnAskContext(AskContextMessage askContextMessage) - { - // Not implemented yet. - return null; - } - private PostResultMessage OnRunCommand(RunCommandMessage runCommandMessage) { // Ignore 'run_command' request when a code posting operation is on-going. @@ -544,14 +540,13 @@ private PostResultMessage OnRunCommand(RunCommandMessage runCommandMessage) _runCommandRequest.Event.Wait(); RunCommandResult result = _runCommandRequest.Result; - _pwsh ??= PowerShell.Create(); - _pwsh.Commands.Clear(); string output = result.ErrorAndOutput.Count is 0 ? string.Empty - : _pwsh.AddCommand("Out-String") + : (_pwsh ??= PowerShell.Create()) + .AddCommand("Out-String") .AddParameter("InputObject", result.ErrorAndOutput) .AddParameter("Width", 120) - .Invoke()[0]; + .InvokeAndCleanup()[0]; PostResultMessage response = new( output: output, @@ -568,6 +563,57 @@ private PostResultMessage OnRunCommand(RunCommandMessage runCommandMessage) return new PostResultMessage(output: _runCommandRequest.Id, hadError: false, userCancelled: false, exception: null); } + private PostResultMessage OnAskCommandOutput(AskCommandOutputMessage askOutputMessage) + { + if (_runCommandRequest is null) + { + return new PostResultMessage( + output: "No command was previously run in background, or the output of a background command was already retrieved.", + hadError: true, + userCancelled: false, + exception: null); + } + + string commandId = askOutputMessage.CommandId; + if (!string.Equals(commandId, _runCommandRequest.Id, StringComparison.OrdinalIgnoreCase)) + { + return new PostResultMessage( + output: $"The specified command id '{commandId}' cannot be found.", + hadError: true, + userCancelled: false, + exception: null); + } + + if (_runCommandRequest.Result is null) + { + return new PostResultMessage( + output: "Command output is not yet available.", + hadError: true, + userCancelled: false, + exception: null); + } + + RunCommandResult result = _runCommandRequest.Result; + string output = result.ErrorAndOutput.Count is 0 + ? string.Empty + : (_pwsh ??= PowerShell.Create()) + .AddCommand("Out-String") + .AddParameter("InputObject", result.ErrorAndOutput) + .AddParameter("Width", 120) + .InvokeAndCleanup()[0]; + + PostResultMessage response = new( + output: output, + hadError: result.HadErrors, + userCancelled: result.UserCancelled, + exception: null); + + _runCommandRequest.Dispose(); + _runCommandRequest = null; + + return response; + } + private void PSRLInsert(string text) { using var _ = new NoWindowResizingCheck(); diff --git a/shell/AIShell.Kernel/Command/CodeCommand.cs b/shell/AIShell.Kernel/Command/CodeCommand.cs index db023fdc..133dbe37 100644 --- a/shell/AIShell.Kernel/Command/CodeCommand.cs +++ b/shell/AIShell.Kernel/Command/CodeCommand.cs @@ -36,13 +36,6 @@ public CodeCommand() copy.SetHandler(CopyAction, nth); save.SetHandler(SaveAction, file, append); post.SetHandler(PostAction, nth); - - - var run = new Command("run", "Run the specified command."); - var command = new Argument("command", "command to run"); - run.AddArgument(command); - run.SetHandler(RunAction, command); - AddCommand(run); } private static string GetCodeText(Shell shell, int index) @@ -188,13 +181,4 @@ private void PostAction(int nth) host.WriteErrorLine(e.Message); } } - - private async Task RunAction(string command) - { - var shell = (Shell)Shell; - var host = shell.Host; - var result = await shell.Channel.RunCommand(new RunCommandMessage(command, blocking: true), shell.CancellationToken); - - host.WriteLine($"HadError: {result.HadError}\nUserCancelled: {result.UserCancelled}\nOutput: {result.Output}\nException: {result.Exception}"); - } } diff --git a/shell/AIShell.Kernel/Host.cs b/shell/AIShell.Kernel/Host.cs index 7199f4cd..d1101254 100644 --- a/shell/AIShell.Kernel/Host.cs +++ b/shell/AIShell.Kernel/Host.cs @@ -570,9 +570,9 @@ internal void RenderReferenceText(string header, string content) /// /// The MCP tool. /// The arguments in JSON form to be sent for the tool call. - internal void RenderToolCallRequest(McpTool tool, string jsonArgs) + internal void RenderMcpToolCallRequest(McpTool tool, string jsonArgs) { - RequireStdoutOrStderr(operation: "render tool call request"); + RequireStdoutOrStderr(operation: "render MCP tool call request"); IAnsiConsole ansiConsole = _outputRedirected ? _stderrConsole : AnsiConsole.Console; bool hasArgs = !string.IsNullOrEmpty(jsonArgs); @@ -610,6 +610,44 @@ internal void RenderToolCallRequest(McpTool tool, string jsonArgs) FancyStreamRender.ConsoleUpdated(); } + /// + /// Render the built-in tool call request. + /// + internal void RenderBuiltInToolCallRequest(string toolName, string description, Tuple argument) + { + RequireStdoutOrStderr(operation: "render built-in tool call request"); + IAnsiConsole ansiConsole = _outputRedirected ? _stderrConsole : AnsiConsole.Console; + + bool hasArgs = argument is not null; + string argLine = hasArgs ? $"{argument.Item1}:" : $"Input: "; + IRenderable content = new Markup($""" + + [bold]Run [olive]{toolName}[/] from [olive]{McpManager.BuiltInServerName}[/] (Built-in tool)[/] + + {description} + + {argLine} + """); + + if (hasArgs) + { + content = new Grid() + .AddColumn(new GridColumn()) + .AddRow(content) + .AddRow(argument.Item2.EscapeMarkup()); + } + + var panel = new Panel(content) + .Expand() + .RoundedBorder() + .Header("[green] Tool Call Request [/]") + .BorderColor(Color.Grey); + + ansiConsole.WriteLine(); + ansiConsole.Write(panel); + FancyStreamRender.ConsoleUpdated(); + } + /// /// Render a table with information about available MCP servers and tools. /// @@ -672,7 +710,13 @@ internal void RenderMcpServersAndTools(McpManager mcpManager) toolTable.AddRow($"[olive underline]{McpManager.BuiltInServerName}[/]", "[green]\u2713 Ready[/]", string.Empty); foreach (var item in mcpManager.BuiltInTools) { - toolTable.AddRow(string.Empty, item.Key.EscapeMarkup(), item.Value.Description.EscapeMarkup()); + string description = item.Value.Description; + int index = description.IndexOf('\n'); + if (index > 0) + { + description = description[..index].Trim(); + } + toolTable.AddRow(string.Empty, item.Key.EscapeMarkup(), description.EscapeMarkup()); } } diff --git a/shell/AIShell.Kernel/MCP/BuiltInTool.cs b/shell/AIShell.Kernel/MCP/BuiltInTool.cs index 45dd3da2..44e9ecec 100644 --- a/shell/AIShell.Kernel/MCP/BuiltInTool.cs +++ b/shell/AIShell.Kernel/MCP/BuiltInTool.cs @@ -1,6 +1,7 @@ using AIShell.Abstraction; using Microsoft.Extensions.AI; using System.Diagnostics; +using System.Text; using System.Text.Json; namespace AIShell.Kernel.Mcp; @@ -16,7 +17,7 @@ private enum ToolType : int copy_text_to_clipboard = 4, post_code_to_terminal = 5, run_command_in_terminal = 6, - get_terminal_output = 7, + get_command_output = 7, NumberOfBuiltInTools = 8 }; @@ -58,15 +59,15 @@ private enum ToolType : int Background Processes: - For long-running tasks (e.g., servers), set `isBackground=true`. - - Returns a terminal ID for checking status and runtime later. + - Returns a command ID for checking status and output later. Important Notes: - If the command may produce excessively large output, use head or tail to reduce the output. - If a command may use a pager, you must add something to disable it. For example, you can use `git --no-pager`. Otherwise you should add something like ` | cat`. Examples: git, less, man, etc. """, - // get_terminal_output - "Get the output of a command previous started with `run_command_in_terminal`" + // get_command_output + "Get the output of a command previously started with `run_command_in_terminal`." ]; private static readonly string[] s_toolSchema = @@ -171,7 +172,7 @@ private enum ToolType : int }, "isBackground": { "type": "boolean", - "description": "Whether the command starts a background process. If true, the command will run in the background and you will not see the output. If false, the tool call will block on the command finishing, and then you will get the output. Examples of backgrond processes: building in watch mode, starting a server. You can check the output of a backgrond process later on by using get_terminal_output." + "description": "Whether the command starts a background process. If true, the command will run in the background and you will not see the output. If false, the tool call will block on the command finishing, and then you will get the output. Examples of backgrond processes: building in watch mode, starting a server. You can check the output of a backgrond process later on by using `get_command_output`." } }, "required": [ @@ -184,7 +185,7 @@ private enum ToolType : int } """, - // get_terminal_output + // get_command_output """ { "type": "object", @@ -248,6 +249,22 @@ protected override async ValueTask InvokeCoreAsync(AIFunctionArguments a return postContextMsg.ContextInfo; } + if (response is PostResultMessage postResultMsg) + { + StringBuilder strb = new(postResultMsg.Output.Length + 40); + strb.AppendLine("### Status") + .AppendLine(postResultMsg.UserCancelled + ? "Execution was cancelled by the user." + : postResultMsg.HadError ? "Had error." : "Succeeded.") + .AppendLine() + .AppendLine("### Output") + .AppendLine("```") + .AppendLine(postResultMsg.Output.Trim()) + .AppendLine("```"); + + return strb.ToString(); + } + return response is null ? "Success: Function completed." : JsonSerializer.SerializeToElement(response); } @@ -267,6 +284,7 @@ internal async Task CallAsync( IReadOnlyDictionary arguments = null, CancellationToken cancellationToken = default) { + PipeMessage response = null; AskContextMessage contextRequest = _toolType switch { ToolType.get_working_directory => new(ContextType.CurrentLocation), @@ -280,12 +298,8 @@ internal async Task CallAsync( _ => null }; - bool succeeded = false; - PostContextMessage response = null; - if (contextRequest is not null) { - succeeded = true; response = await _shell.Host.RunWithSpinnerAsync( async () => await _shell.Channel.AskContext(contextRequest, cancellationToken), status: $"Running '{_toolName}'", @@ -294,39 +308,74 @@ internal async Task CallAsync( else if (_toolType is ToolType.copy_text_to_clipboard) { TryGetArgumentValue(arguments, "content", out string content); - if (string.IsNullOrEmpty(content)) { throw new ArgumentException("The 'content' argument is required for the 'copy_text_to_clipboard' tool."); } - succeeded = true; Clipboard.SetText(content); } else if (_toolType is ToolType.post_code_to_terminal) { TryGetArgumentValue(arguments, "command", out string command); - if (string.IsNullOrEmpty(command)) { throw new ArgumentException("The 'command' argument is required for the 'post_code_to_terminal' tool."); } - succeeded = true; _shell.Channel.PostCode(new PostCodeMessage([command])); } + else if (_toolType is ToolType.run_command_in_terminal) + { + TryGetArgumentValue(arguments, "command", out string command); + TryGetArgumentValue(arguments, "explanation", out string explanation); + TryGetArgumentValue(arguments, "isBackground", out bool isBackground); - if (succeeded) + if (string.IsNullOrEmpty(command)) + { + throw new ArgumentException("The 'command' argument is required for the 'run_command_in_terminal' tool."); + } + if (string.IsNullOrEmpty(explanation)) + { + throw new ArgumentException("The 'explanation' argument is required for the 'run_command_in_terminal' tool."); + } + + _shell.Host.RenderBuiltInToolCallRequest(OriginalName, explanation, Tuple.Create("command", command)); + // Prompt for user's approval to call the tool. + const string title = "\n\u26A0 Malicious converstaion content may attempt to misuse 'AIShell' through the built-in tools. Please carefully review any requested actions to decide if you want to proceed."; + string choice = await _shell.Host.PromptForSelectionAsync( + title: title, + choices: McpTool.UserChoices, + cancellationToken: cancellationToken); + + if (choice is "Cancel") + { + _shell.Host.MarkupLine($"\n [red]\u2717[/] Cancelled '{OriginalName}'"); + throw new OperationCanceledException("The call was rejected by user."); + } + + response = await _shell.Host.RunWithSpinnerAsync( + async () => await _shell.Channel.RunCommand(new RunCommandMessage(command, blocking: !isBackground), cancellationToken), + status: $"Running '{_toolName}'", + spinnerKind: SpinnerKind.Processing); + } + else if (_toolType is ToolType.get_command_output) { - // Notify the user about this tool call. - _shell.Host.MarkupLine($"\n [green]\u2713[/] Ran '{_toolName}'"); - // Signal any active stream reander about the output - FancyStreamRender.ConsoleUpdated(); + TryGetArgumentValue(arguments, "id", out string id); + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentException("The 'id' argument is required for the 'get_command_output' tool."); + } - return response; + response = await _shell.Channel.AskCommandOutput(new AskCommandOutputMessage(id), cancellationToken); } - throw new NotSupportedException($"Tool type '{_toolType}' is not yet supported."); + // Notify the user about this tool call. + _shell.Host.MarkupLine($"\n [green]\u2713[/] Ran '{_toolName}'"); + + // Signal any active stream render about the output. + FancyStreamRender.ConsoleUpdated(); + return response; } private static bool TryGetArgumentValue(IReadOnlyDictionary arguments, string argName, out T value) @@ -398,8 +447,8 @@ internal static Dictionary GetBuiltInTools(Shell shell) { ArgumentNullException.ThrowIfNull(shell); - // We don't have the 'run_command' and 'get_terminal_output' tools yet. Will use 'ToolType.NumberOfBuiltInTools' when all tools are ready. - int toolCount = (int)ToolType.run_command_in_terminal; + // We don't have the 'run_command' and 'get_command_output' tools yet. Will use 'ToolType.NumberOfBuiltInTools' when all tools are ready. + int toolCount = (int)ToolType.NumberOfBuiltInTools; Debug.Assert(s_toolDescription.Length == (int)ToolType.NumberOfBuiltInTools, "Number of tool descriptions doesn't match the number of tools."); Debug.Assert(s_toolSchema.Length == (int)ToolType.NumberOfBuiltInTools, "Number of tool schemas doesn't match the number of tools."); diff --git a/shell/AIShell.Kernel/MCP/McpTool.cs b/shell/AIShell.Kernel/MCP/McpTool.cs index 97142485..e52235c9 100644 --- a/shell/AIShell.Kernel/MCP/McpTool.cs +++ b/shell/AIShell.Kernel/MCP/McpTool.cs @@ -113,7 +113,7 @@ internal async ValueTask CallAsync( string jsonArgs = arguments is { Count: > 0 } ? JsonSerializer.Serialize(arguments, serializerOptions ?? JsonSerializerOptions) : null; - _host.RenderToolCallRequest(this, jsonArgs); + _host.RenderMcpToolCallRequest(this, jsonArgs); // Prompt for user's approval to call the tool. const string title = "\n\u26A0 MCP servers or malicious converstaion content may attempt to misuse 'AIShell' through the installed tools. Please carefully review any requested actions to decide if you want to proceed."; diff --git a/shell/AIShell.Kernel/ShellIntegration/Channel.cs b/shell/AIShell.Kernel/ShellIntegration/Channel.cs index e2311141..78249645 100644 --- a/shell/AIShell.Kernel/ShellIntegration/Channel.cs +++ b/shell/AIShell.Kernel/ShellIntegration/Channel.cs @@ -221,6 +221,15 @@ internal async Task RunCommand(RunCommandMessage message, Can return await _clientPipe.RunCommand(message, cancellationToken); } + /// + /// Ask for the output of a command that was previously run in the connected shell. + /// + internal async Task AskCommandOutput(AskCommandOutputMessage message, CancellationToken cancellationToken) + { + ThrowIfNotConnected(); + return await _clientPipe.AskCommandOutput(message, cancellationToken); + } + public void Dispose() { if (_disposed) From cac864c65201c9942a7f0c26ab7bd0ff43c8bac9 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 14 Jul 2025 14:49:48 -0700 Subject: [PATCH 3/6] Update the system prompt to call out the ability to run commands. --- shell/agents/AIShell.OpenAI.Agent/Helpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/agents/AIShell.OpenAI.Agent/Helpers.cs b/shell/agents/AIShell.OpenAI.Agent/Helpers.cs index c4df2e5e..93e2f253 100644 --- a/shell/agents/AIShell.OpenAI.Agent/Helpers.cs +++ b/shell/agents/AIShell.OpenAI.Agent/Helpers.cs @@ -195,7 +195,7 @@ internal static class Prompt internal static string SystemPromptWithConnectedPSSession = $""" You are a virtual assistant in **AIShell**, specializing in PowerShell and other command-line tools. - You are connected to an interactive PowerShell session and can retrieve session context and interact with the session using built-in tools. When user queries are ambiguous or minimal, rely on session context to better understand intent and deliver accurate, helpful responses.. + You are connected to an interactive PowerShell session and can retrieve session context and run commands in the session using built-in tools. When user queries are ambiguous or minimal, rely on session context to better understand intent and deliver accurate, helpful responses. Your primary function is to assist users with accomplishing tasks and troubleshooting errors in the command line. Autonomously resolve the user's query to the best of your ability before returning with a response. From 013ff076191cd0d75e6c8eab5ee20d47237402bc Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 15 Jul 2025 12:22:10 -0700 Subject: [PATCH 4/6] Fix a typo --- shell/AIShell.Kernel/MCP/BuiltInTool.cs | 2 +- shell/AIShell.Kernel/MCP/McpTool.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/AIShell.Kernel/MCP/BuiltInTool.cs b/shell/AIShell.Kernel/MCP/BuiltInTool.cs index 44e9ecec..ed7f65a4 100644 --- a/shell/AIShell.Kernel/MCP/BuiltInTool.cs +++ b/shell/AIShell.Kernel/MCP/BuiltInTool.cs @@ -342,7 +342,7 @@ internal async Task CallAsync( _shell.Host.RenderBuiltInToolCallRequest(OriginalName, explanation, Tuple.Create("command", command)); // Prompt for user's approval to call the tool. - const string title = "\n\u26A0 Malicious converstaion content may attempt to misuse 'AIShell' through the built-in tools. Please carefully review any requested actions to decide if you want to proceed."; + const string title = "\n\u26A0 Malicious conversation content may attempt to misuse 'AIShell' through the built-in tools. Please carefully review any requested actions to decide if you want to proceed."; string choice = await _shell.Host.PromptForSelectionAsync( title: title, choices: McpTool.UserChoices, diff --git a/shell/AIShell.Kernel/MCP/McpTool.cs b/shell/AIShell.Kernel/MCP/McpTool.cs index e52235c9..a78a21ac 100644 --- a/shell/AIShell.Kernel/MCP/McpTool.cs +++ b/shell/AIShell.Kernel/MCP/McpTool.cs @@ -116,7 +116,7 @@ internal async ValueTask CallAsync( _host.RenderMcpToolCallRequest(this, jsonArgs); // Prompt for user's approval to call the tool. - const string title = "\n\u26A0 MCP servers or malicious converstaion content may attempt to misuse 'AIShell' through the installed tools. Please carefully review any requested actions to decide if you want to proceed."; + const string title = "\n\u26A0 MCP servers or malicious conversation content may attempt to misuse 'AIShell' through the installed tools. Please carefully review any requested actions to decide if you want to proceed."; string choice = await _host.PromptForSelectionAsync( title: title, choices: UserChoices, From 1fb4b7b5dde8750704d8e379ab863a3e7c215f53 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 23 Jul 2025 15:20:05 -0700 Subject: [PATCH 5/6] Ignore unexpected exceptions when retrieving history commands from the connected PS session --- shell/AIShell.Integration/Channel.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shell/AIShell.Integration/Channel.cs b/shell/AIShell.Integration/Channel.cs index 1306d563..28e63678 100644 --- a/shell/AIShell.Integration/Channel.cs +++ b/shell/AIShell.Integration/Channel.cs @@ -212,6 +212,10 @@ private void RunspaceAvailableAction(object sender, RunspaceAvailabilityEventArg _commandHistory.AddRange(results); } } + catch + { + // Ignore unexpected exceptions. + } finally { pwshRunspace.AvailabilityChanged += RunspaceAvailableAction; From 9c7c151187bf0f94f0a6711f5ab44244e28c871e Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 23 Jul 2025 16:54:20 -0700 Subject: [PATCH 6/6] Remove an outdated comment --- shell/AIShell.Kernel/MCP/BuiltInTool.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/shell/AIShell.Kernel/MCP/BuiltInTool.cs b/shell/AIShell.Kernel/MCP/BuiltInTool.cs index ed7f65a4..49580da7 100644 --- a/shell/AIShell.Kernel/MCP/BuiltInTool.cs +++ b/shell/AIShell.Kernel/MCP/BuiltInTool.cs @@ -447,7 +447,6 @@ internal static Dictionary GetBuiltInTools(Shell shell) { ArgumentNullException.ThrowIfNull(shell); - // We don't have the 'run_command' and 'get_command_output' tools yet. Will use 'ToolType.NumberOfBuiltInTools' when all tools are ready. int toolCount = (int)ToolType.NumberOfBuiltInTools; Debug.Assert(s_toolDescription.Length == (int)ToolType.NumberOfBuiltInTools, "Number of tool descriptions doesn't match the number of tools."); Debug.Assert(s_toolSchema.Length == (int)ToolType.NumberOfBuiltInTools, "Number of tool schemas doesn't match the number of tools.");