using System; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using ClaudeDo.Ui.Services; using Iciclecreek.Terminal; namespace ClaudeDo.Ui.ViewModels; /// /// Hosts a for an embedded ConPTY terminal. The view attaches /// its once loaded via ; /// can be called before or after attach — whichever happens second triggers the launch. /// public sealed partial class InteractiveTerminalViewModel : ViewModelBase, IDisposable { private readonly PtyTerminalSession _session = new(); private TerminalControl? _control; private TerminalLaunchDescriptor? _pendingDescriptor; [ObservableProperty] private bool _isRunning; [ObservableProperty] private bool _hasExited; [ObservableProperty] private int? _exitCode; [ObservableProperty] private string? _startError; /// True from construction until the child process is actually launched (or the launch /// failed) — covers both the caller's launch-spec roundtrip and the ConPTY spawn, so the host /// can show a spinner instead of an empty black pane. public bool IsStarting => !IsRunning && !HasExited && StartError is null; partial void OnIsRunningChanged(bool value) => OnPropertyChanged(nameof(IsStarting)); partial void OnHasExitedChanged(bool value) => OnPropertyChanged(nameof(IsStarting)); partial void OnStartErrorChanged(string? value) => OnPropertyChanged(nameof(IsStarting)); public InteractiveTerminalViewModel() { _session.ProcessExited += OnSessionProcessExited; } /// Called by the view once its has loaded (template applied). public void AttachControl(TerminalControl control) { _control = control; if (_pendingDescriptor is { } descriptor) { _pendingDescriptor = null; _ = StartCoreAsync(descriptor); } } public void Start(TerminalLaunchDescriptor descriptor) { if (_control is null) { _pendingDescriptor = descriptor; return; } _ = StartCoreAsync(descriptor); } private async Task StartCoreAsync(TerminalLaunchDescriptor descriptor) { if (_control is null) return; try { StartError = null; await _session.StartAsync(descriptor, _control); IsRunning = true; HasExited = false; ExitCode = null; } catch (Exception ex) { IsRunning = false; HasExited = true; StartError = ex.Message; } } private void OnSessionProcessExited(object? sender, int exitCode) { IsRunning = false; HasExited = true; ExitCode = exitCode; } /// Reports a failure that happened before could be called (e.g. the /// launch-spec roundtrip threw), so it surfaces through the same banner as a spawn failure. public void ReportStartFailure(string message) { IsRunning = false; HasExited = true; StartError = message; } public void Kill() => _session.Kill(); public void Dispose() { _session.ProcessExited -= OnSessionProcessExited; _session.Dispose(); } }