Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/OneWare.Chat/Services/AiBuiltInFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ internal static class AiBuiltInFunctions
private const int MaxTerminalOutputLines = 220;
private const int MaxTerminalOutputChars = 12000;

private static readonly TimeSpan TerminalCommandTimeout = TimeSpan.FromHours(12);

public static void Register(
IAiFunctionProvider functionProvider,
IProjectExplorerService projectExplorerService,
Expand Down Expand Up @@ -149,8 +151,10 @@ Output is automatically truncated to avoid oversized responses.
""",
Handler = ([Description("Shell command to execute")] string command,
[Description("Absolute working directory for execution (optional, defaults to active project).")]
string? workDir = null) =>
RunTerminalCommandAsync(projectExplorerService, terminalManagerService, command, workDir),
string? workDir = null,
CancellationToken cancellationToken = default) =>
RunTerminalCommandAsync(projectExplorerService, terminalManagerService, command, workDir,
cancellationToken),
ConfirmationCheck = args =>
{
var cmd = TryGetStringArgument(args, "command") ?? "?";
Expand Down Expand Up @@ -239,7 +243,8 @@ private static async Task<object> RunTerminalCommandAsync(
IProjectExplorerService projectExplorerService,
ITerminalManagerService terminalManagerService,
string command,
string? workDir)
string? workDir,
CancellationToken cancellationToken)
{
var resolvedWorkDir = ResolvePath(projectExplorerService, workDir);

Expand All @@ -248,7 +253,8 @@ private static async Task<object> RunTerminalCommandAsync(
"AI Chat",
resolvedWorkDir,
true,
TimeSpan.FromMinutes(1));
TerminalCommandTimeout,
cancellationToken);

var truncatedOutput = TruncateTerminalOutput(terminalResult.Output, out var outputTruncated);
var result = outputTruncated
Expand Down
32 changes: 29 additions & 3 deletions src/OneWare.Terminal/Provider/Unix/UnixPseudoTerminal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,36 @@ public async Task WriteAsync(byte[] buffer, int offset, int count)
await Task.Run(() =>
{
var buf = Marshal.AllocHGlobal(count);
Marshal.Copy(buffer, offset, buf, count);
Native.write(_cfg, buf, count);
try
{
Marshal.Copy(buffer, offset, buf, count);

// POSIX write() may perform a partial write or return -1 on EINTR/EAGAIN
// when the pty input buffer is momentarily full. Ignoring that dropped
// part of the command (often the trailing carriage return), leaving the
// shell waiting for input and the command hanging forever. Loop until
// every byte is written so command submission is reliable.
var written = 0;
var retries = 0;
while (written < count && !_isDisposed)
{
var result = Native.write(_cfg, IntPtr.Add(buf, written), count - written);

if (result > 0)
{
written += result;
retries = 0;
continue;
}

Marshal.FreeHGlobal(buf);
if (++retries > 1000) break;
Thread.Sleep(1);
}
}
finally
{
Marshal.FreeHGlobal(buf);
}
});
}

Expand Down
7 changes: 7 additions & 0 deletions src/OneWare.Terminal/ViewModels/TerminalViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ public void Send(string command)
if (Connection?.IsConnected ?? false) Connection.SendData(Encoding.ASCII.GetBytes($"{command}\r"));
}

public void SendInterrupt()
{
// Send Ctrl+C (ETX) to abort the currently running foreground command
// so the shell returns to a usable prompt.
if (Connection?.IsConnected ?? false) Connection.SendData([0x03]);
}

public void SuppressEcho(byte[] data)
{
if (Connection is IOutputSuppressor suppressor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,13 @@ void OnDataReceived(object? sender, VtNetCore.Avalonia.DataReceivedEventArgs arg
}
catch (OperationCanceledException)
{
result = new TerminalExecutionResult(output.ToString(), -1, true);
var partialOutput = output.ToString();
// The command exceeded its timeout or was cancelled but is still running
// in the shell. Interrupt it so the shell returns to a usable prompt;
// otherwise the foreground command keeps running and every subsequent
// command sent to this (reused) terminal hangs too.
await TryRecoverPromptAsync(terminal, resultTcs.Task);
result = new TerminalExecutionResult(partialOutput, -1, true);
}
finally
{
Expand All @@ -179,6 +185,22 @@ void OnDataReceived(object? sender, VtNetCore.Avalonia.DataReceivedEventArgs arg
return result;
}

private static async Task TryRecoverPromptAsync(TerminalViewModel terminal,
Task<TerminalExecutionResult> resultTask)
{
terminal.SendInterrupt();
try
{
// Give the shell a moment to process the interrupt and emit a fresh
// prompt marker so the terminal can be reused by the next command.
await resultTask.WaitAsync(TimeSpan.FromSeconds(3));
}
catch (TimeoutException)
{
// Best-effort recovery: fall through even if the shell did not respond.
}
}

public void ExecScriptInTerminal(string scriptPath, bool elevated, string title)
{
try
Expand Down
Loading