Skip to content

Commit 671a215

Browse files
committed
Enable 'run_command_in_terminal' and 'get_command_output' tools
1 parent 2e2fe08 commit 671a215

File tree

7 files changed

+287
-54
lines changed

7 files changed

+287
-54
lines changed

shell/AIShell.Abstraction/NamedPipe.cs

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,15 @@ public enum MessageType : int
3939
/// </summary>
4040
RunCommand = 5,
4141

42+
/// <summary>
43+
/// A message from AIShell to command-line shell to ask for the result of a previous command run.
44+
/// </summary>
45+
AskCommandOutput = 6,
46+
4247
/// <summary>
4348
/// A message from command-line shell to AIShell to post the result of a command.
4449
/// </summary>
45-
PostResult = 6,
50+
PostResult = 7,
4651
}
4752

4853
/// <summary>
@@ -239,6 +244,27 @@ public RunCommandMessage(string command, bool blocking)
239244
}
240245
}
241246

247+
/// <summary>
248+
/// Message for <see cref="MessageType.AskCommandOutput"/>.
249+
/// </summary>
250+
public sealed class AskCommandOutputMessage : PipeMessage
251+
{
252+
/// <summary>
253+
/// Gets the id of the command to retrieve the output for.
254+
/// </summary>
255+
public string CommandId { get; }
256+
257+
/// <summary>
258+
/// Creates an instance of <see cref="AskCommandOutputMessage"/>.
259+
/// </summary>
260+
public AskCommandOutputMessage(string commandId)
261+
: base(MessageType.AskCommandOutput)
262+
{
263+
ArgumentException.ThrowIfNullOrEmpty(commandId);
264+
CommandId = commandId;
265+
}
266+
}
267+
242268
/// <summary>
243269
/// Message for <see cref="MessageType.PostResult"/>.
244270
/// </summary>
@@ -426,6 +452,7 @@ private static PipeMessage DeserializePayload(int type, ReadOnlySpan<byte> bytes
426452
(int)MessageType.PostContext => JsonSerializer.Deserialize<PostContextMessage>(bytes),
427453
(int)MessageType.PostCode => JsonSerializer.Deserialize<PostCodeMessage>(bytes),
428454
(int)MessageType.RunCommand => JsonSerializer.Deserialize<RunCommandMessage>(bytes),
455+
(int)MessageType.AskCommandOutput => JsonSerializer.Deserialize<AskCommandOutputMessage>(bytes),
429456
(int)MessageType.PostResult => JsonSerializer.Deserialize<PostResultMessage>(bytes),
430457
_ => throw new NotSupportedException("Unreachable code"),
431458
};
@@ -550,6 +577,11 @@ public async Task StartProcessingAsync(int timeout, CancellationToken cancellati
550577
SendMessage(result);
551578
break;
552579

580+
case MessageType.AskCommandOutput:
581+
var output = InvokeOnAskCommandOutput((AskCommandOutputMessage)message);
582+
SendMessage(output);
583+
break;
584+
553585
default:
554586
// Log: unexpected messages ignored.
555587
break;
@@ -622,6 +654,9 @@ private PostContextMessage InvokeOnAskContext(AskContextMessage message)
622654
return null;
623655
}
624656

657+
/// <summary>
658+
/// Helper to invoke the <see cref="OnRunCommand"/> event.
659+
/// </summary>
625660
private PostResultMessage InvokeOnRunCommand(RunCommandMessage message)
626661
{
627662
if (OnRunCommand is null)
@@ -649,6 +684,36 @@ private PostResultMessage InvokeOnRunCommand(RunCommandMessage message)
649684
}
650685
}
651686

687+
/// <summary>
688+
/// Helper to invoke the <see cref="OnAskCommandOutput"/> event.
689+
/// </summary>
690+
private PostResultMessage InvokeOnAskCommandOutput(AskCommandOutputMessage message)
691+
{
692+
if (OnAskCommandOutput is null)
693+
{
694+
// Log: event handler not set.
695+
return new PostResultMessage(
696+
output: "Retrieving command output is not supported.",
697+
hadError: true,
698+
userCancelled: false,
699+
exception: null);
700+
}
701+
702+
try
703+
{
704+
return OnAskCommandOutput(message);
705+
}
706+
catch (Exception e)
707+
{
708+
// Log: exception when invoking 'OnAskCommandOutput'
709+
return new PostResultMessage(
710+
output: "Failed to retrieve the command output due to an internal error.",
711+
hadError: true,
712+
userCancelled: false,
713+
exception: e.Message);
714+
}
715+
}
716+
652717
/// <summary>
653718
/// Event for handling the <see cref="MessageType.PostCode"/> message.
654719
/// </summary>
@@ -668,6 +733,11 @@ private PostResultMessage InvokeOnRunCommand(RunCommandMessage message)
668733
/// Event for handling the <see cref="MessageType.RunCommand"/> message.
669734
/// </summary>
670735
public event Func<RunCommandMessage, PostResultMessage> OnRunCommand;
736+
737+
/// <summary>
738+
/// Event for handling the <see cref="MessageType.AskCommandOutput"/> message.
739+
/// </summary>
740+
public event Func<AskCommandOutputMessage, PostResultMessage> OnAskCommandOutput;
671741
}
672742

673743
/// <summary>
@@ -889,6 +959,13 @@ public async Task<PostContextMessage> AskContext(AskContextMessage message, Canc
889959
return postContext;
890960
}
891961

962+
/// <summary>
963+
/// Run a command in the connected PowerShell session.
964+
/// </summary>
965+
/// <param name="message">The <see cref="MessageType.RunCommand"/> message.</param>
966+
/// <param name="cancellationToken">A cancellation token.</param>
967+
/// <returns>A <see cref="MessageType.PostResult"/> message as the response.</returns>
968+
/// <exception cref="IOException">Throws when the pipe is closed by the other side.</exception>
892969
public async Task<PostResultMessage> RunCommand(RunCommandMessage message, CancellationToken cancellationToken)
893970
{
894971
// Send the request message to the shell.
@@ -905,4 +982,28 @@ public async Task<PostResultMessage> RunCommand(RunCommandMessage message, Cance
905982

906983
return postResult;
907984
}
985+
986+
/// <summary>
987+
/// Ask for the output of a previously run command in the connected PowerShell session.
988+
/// </summary>
989+
/// <param name="message">The <see cref="MessageType.AskCommandOutput"/> message.</param>
990+
/// <param name="cancellationToken">A cancellation token.</param>
991+
/// <returns>A <see cref="MessageType.PostResult"/> message as the response.</returns>
992+
/// <exception cref="IOException">Throws when the pipe is closed by the other side.</exception>
993+
public async Task<PostResultMessage> AskCommandOutput(AskCommandOutputMessage message, CancellationToken cancellationToken)
994+
{
995+
// Send the request message to the shell.
996+
SendMessage(message);
997+
998+
// Receiving response from the shell.
999+
var response = await GetMessageAsync(cancellationToken);
1000+
if (response is not PostResultMessage postResult)
1001+
{
1002+
// Log: unexpected message. drop connection.
1003+
_client.Close();
1004+
throw new IOException($"Expecting '{MessageType.PostResult}' response, but received '{message.Type}' message.");
1005+
}
1006+
1007+
return postResult;
1008+
}
9081009
}

shell/AIShell.Integration/Channel.cs

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ public string StartChannelSetup()
151151
_serverPipe.OnAskContext += OnAskContext;
152152
_serverPipe.OnPostCode += OnPostCode;
153153
_serverPipe.OnRunCommand += OnRunCommand;
154+
_serverPipe.OnAskCommandOutput += OnAskCommandOutput;
154155

155156
_serverThread = new Thread(ThreadProc)
156157
{
@@ -284,6 +285,7 @@ private void Reset()
284285
_serverPipe.OnAskContext -= OnAskContext;
285286
_serverPipe.OnPostCode -= OnPostCode;
286287
_serverPipe.OnRunCommand -= OnRunCommand;
288+
_serverPipe.OnAskCommandOutput -= OnAskCommandOutput;
287289
}
288290

289291
_serverPipe = null;
@@ -488,12 +490,6 @@ private void OnAskConnection(ShellClientPipe clientPipe, Exception exception)
488490
_connSetupWaitHandler.Set();
489491
}
490492

491-
private PostContextMessage OnAskContext(AskContextMessage askContextMessage)
492-
{
493-
// Not implemented yet.
494-
return null;
495-
}
496-
497493
private PostResultMessage OnRunCommand(RunCommandMessage runCommandMessage)
498494
{
499495
// Ignore 'run_command' request when a code posting operation is on-going.
@@ -544,14 +540,13 @@ private PostResultMessage OnRunCommand(RunCommandMessage runCommandMessage)
544540
_runCommandRequest.Event.Wait();
545541
RunCommandResult result = _runCommandRequest.Result;
546542

547-
_pwsh ??= PowerShell.Create();
548-
_pwsh.Commands.Clear();
549543
string output = result.ErrorAndOutput.Count is 0
550544
? string.Empty
551-
: _pwsh.AddCommand("Out-String")
545+
: (_pwsh ??= PowerShell.Create())
546+
.AddCommand("Out-String")
552547
.AddParameter("InputObject", result.ErrorAndOutput)
553548
.AddParameter("Width", 120)
554-
.Invoke<string>()[0];
549+
.InvokeAndCleanup<string>()[0];
555550

556551
PostResultMessage response = new(
557552
output: output,
@@ -568,6 +563,57 @@ private PostResultMessage OnRunCommand(RunCommandMessage runCommandMessage)
568563
return new PostResultMessage(output: _runCommandRequest.Id, hadError: false, userCancelled: false, exception: null);
569564
}
570565

566+
private PostResultMessage OnAskCommandOutput(AskCommandOutputMessage askOutputMessage)
567+
{
568+
if (_runCommandRequest is null)
569+
{
570+
return new PostResultMessage(
571+
output: "No command was previously run in background, or the output of a background command was already retrieved.",
572+
hadError: true,
573+
userCancelled: false,
574+
exception: null);
575+
}
576+
577+
string commandId = askOutputMessage.CommandId;
578+
if (!string.Equals(commandId, _runCommandRequest.Id, StringComparison.OrdinalIgnoreCase))
579+
{
580+
return new PostResultMessage(
581+
output: $"The specified command id '{commandId}' cannot be found.",
582+
hadError: true,
583+
userCancelled: false,
584+
exception: null);
585+
}
586+
587+
if (_runCommandRequest.Result is null)
588+
{
589+
return new PostResultMessage(
590+
output: "Command output is not yet available.",
591+
hadError: true,
592+
userCancelled: false,
593+
exception: null);
594+
}
595+
596+
RunCommandResult result = _runCommandRequest.Result;
597+
string output = result.ErrorAndOutput.Count is 0
598+
? string.Empty
599+
: (_pwsh ??= PowerShell.Create())
600+
.AddCommand("Out-String")
601+
.AddParameter("InputObject", result.ErrorAndOutput)
602+
.AddParameter("Width", 120)
603+
.InvokeAndCleanup<string>()[0];
604+
605+
PostResultMessage response = new(
606+
output: output,
607+
hadError: result.HadErrors,
608+
userCancelled: result.UserCancelled,
609+
exception: null);
610+
611+
_runCommandRequest.Dispose();
612+
_runCommandRequest = null;
613+
614+
return response;
615+
}
616+
571617
private void PSRLInsert(string text)
572618
{
573619
using var _ = new NoWindowResizingCheck();

shell/AIShell.Kernel/Command/CodeCommand.cs

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,6 @@ public CodeCommand()
3636
copy.SetHandler(CopyAction, nth);
3737
save.SetHandler(SaveAction, file, append);
3838
post.SetHandler(PostAction, nth);
39-
40-
41-
var run = new Command("run", "Run the specified command.");
42-
var command = new Argument<string>("command", "command to run");
43-
run.AddArgument(command);
44-
run.SetHandler(RunAction, command);
45-
AddCommand(run);
4639
}
4740

4841
private static string GetCodeText(Shell shell, int index)
@@ -188,13 +181,4 @@ private void PostAction(int nth)
188181
host.WriteErrorLine(e.Message);
189182
}
190183
}
191-
192-
private async Task RunAction(string command)
193-
{
194-
var shell = (Shell)Shell;
195-
var host = shell.Host;
196-
var result = await shell.Channel.RunCommand(new RunCommandMessage(command, blocking: true), shell.CancellationToken);
197-
198-
host.WriteLine($"HadError: {result.HadError}\nUserCancelled: {result.UserCancelled}\nOutput: {result.Output}\nException: {result.Exception}");
199-
}
200184
}

shell/AIShell.Kernel/Host.cs

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -570,9 +570,9 @@ internal void RenderReferenceText(string header, string content)
570570
/// </summary>
571571
/// <param name="tool">The MCP tool.</param>
572572
/// <param name="jsonArgs">The arguments in JSON form to be sent for the tool call.</param>
573-
internal void RenderToolCallRequest(McpTool tool, string jsonArgs)
573+
internal void RenderMcpToolCallRequest(McpTool tool, string jsonArgs)
574574
{
575-
RequireStdoutOrStderr(operation: "render tool call request");
575+
RequireStdoutOrStderr(operation: "render MCP tool call request");
576576
IAnsiConsole ansiConsole = _outputRedirected ? _stderrConsole : AnsiConsole.Console;
577577

578578
bool hasArgs = !string.IsNullOrEmpty(jsonArgs);
@@ -610,6 +610,44 @@ internal void RenderToolCallRequest(McpTool tool, string jsonArgs)
610610
FancyStreamRender.ConsoleUpdated();
611611
}
612612

613+
/// <summary>
614+
/// Render the built-in tool call request.
615+
/// </summary>
616+
internal void RenderBuiltInToolCallRequest(string toolName, string description, Tuple<string, string> argument)
617+
{
618+
RequireStdoutOrStderr(operation: "render built-in tool call request");
619+
IAnsiConsole ansiConsole = _outputRedirected ? _stderrConsole : AnsiConsole.Console;
620+
621+
bool hasArgs = argument is not null;
622+
string argLine = hasArgs ? $"{argument.Item1}:" : $"Input: <none>";
623+
IRenderable content = new Markup($"""
624+
625+
[bold]Run [olive]{toolName}[/] from [olive]{McpManager.BuiltInServerName}[/] (Built-in tool)[/]
626+
627+
{description}
628+
629+
{argLine}
630+
""");
631+
632+
if (hasArgs)
633+
{
634+
content = new Grid()
635+
.AddColumn(new GridColumn())
636+
.AddRow(content)
637+
.AddRow(argument.Item2.EscapeMarkup());
638+
}
639+
640+
var panel = new Panel(content)
641+
.Expand()
642+
.RoundedBorder()
643+
.Header("[green] Tool Call Request [/]")
644+
.BorderColor(Color.Grey);
645+
646+
ansiConsole.WriteLine();
647+
ansiConsole.Write(panel);
648+
FancyStreamRender.ConsoleUpdated();
649+
}
650+
613651
/// <summary>
614652
/// Render a table with information about available MCP servers and tools.
615653
/// </summary>
@@ -672,7 +710,13 @@ internal void RenderMcpServersAndTools(McpManager mcpManager)
672710
toolTable.AddRow($"[olive underline]{McpManager.BuiltInServerName}[/]", "[green]\u2713 Ready[/]", string.Empty);
673711
foreach (var item in mcpManager.BuiltInTools)
674712
{
675-
toolTable.AddRow(string.Empty, item.Key.EscapeMarkup(), item.Value.Description.EscapeMarkup());
713+
string description = item.Value.Description;
714+
int index = description.IndexOf('\n');
715+
if (index > 0)
716+
{
717+
description = description[..index].Trim();
718+
}
719+
toolTable.AddRow(string.Empty, item.Key.EscapeMarkup(), description.EscapeMarkup());
676720
}
677721
}
678722

0 commit comments

Comments
 (0)