Files
ClaudeDo/src/ClaudeDo.Ui/Services/PtyTerminalSession.cs
T
mika kuns 2b06ab0ab4 fix(interactive): use library LaunchProcess instead of custom pty bypass
The custom Porta.Pty bypass (own read loop, key tunneling, resize sync) rendered
wrong, lagged, and dropped input. The spike proved TerminalControl.LaunchProcess()
renders correctly and stays responsive, so hand pty/input/render/resize/focus back
to the library. PtyTerminalSession shrinks to a thin wrapper: apply descriptor.Env
process-wide (Porta.Pty inherits the process env; no per-launch env seam), set
Process/Args/StartingDirectory, LaunchProcess(). Process="" still suppresses the
control's auto-launch so exactly one process starts.
2026-07-23 16:47:15 +02:00

74 lines
2.6 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Iciclecreek.Terminal;
namespace ClaudeDo.Ui.Services;
/// <summary>
/// Thin wrapper around <see cref="TerminalControl.LaunchProcess()"/> — the library owns the
/// Porta.Pty spawn, keyboard input, rendering, resize, and focus end to end (see
/// <c>spikes/ConPtyTerminal/MainWindow.axaml.cs</c>, which proved this renders correctly and
/// stays responsive). We only:
/// 1) apply <see cref="TerminalLaunchDescriptor.Env"/> onto the current process environment
/// before launching — Porta.Pty inherits the calling process's environment and there is no
/// per-launch env seam on <see cref="TerminalControl"/>/<see cref="TerminalControl.LaunchProcess()"/> —
/// 2) relay the control's own <see cref="TerminalControl.ProcessExited"/> event and
/// <see cref="TerminalControl.Kill()"/> method.
/// </summary>
public sealed class PtyTerminalSession : IDisposable
{
private TerminalControl? _control;
private bool _disposed;
public bool IsRunning { get; private set; }
public int? ExitCode { get; private set; }
/// <summary>Raised when the child process exits, on the UI thread, with its exit code.</summary>
public event EventHandler<int>? ProcessExited;
/// <summary>
/// Applies <paramref name="descriptor"/>'s env vars to the current process, then drives
/// <paramref name="control"/> to launch it via <see cref="TerminalControl.LaunchProcess()"/>.
/// </summary>
public async Task StartAsync(TerminalLaunchDescriptor descriptor, TerminalControl control, CancellationToken ct = default)
{
if (_control is not null) throw new InvalidOperationException("Session already started.");
_control = control;
control.ProcessExited += OnControlProcessExited;
foreach (var (key, value) in descriptor.Env)
Environment.SetEnvironmentVariable(key, value);
control.Process = descriptor.Exe;
control.Args = new List<string>(descriptor.Args);
control.StartingDirectory = descriptor.Cwd;
await control.LaunchProcess();
IsRunning = true;
}
private void OnControlProcessExited(object? sender, ProcessExitedEventArgs e)
{
IsRunning = false;
ExitCode = e.ExitCode;
ProcessExited?.Invoke(this, e.ExitCode);
}
public void Kill()
{
try { _control?.Kill(); }
catch (Exception) { /* already exited */ }
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
if (_control is not null)
_control.ProcessExited -= OnControlProcessExited;
}
}