using System; using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using ClaudeDo.Ui.Services; namespace ClaudeDo.Ui.ViewModels.MissionControl; /// /// Command Center pane hosting an embedded ConPTY terminal for either one task's interactive /// Claude session, or an ad-hoc/free session in a user-chosen directory (no task, /// is null — ad-hoc panes are never deduped, unlike task-based ones). /// public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControlPane, IDisposable { public string? TaskId { get; } // Only a task-based pane can be submitted for review (an ad-hoc directory session has no task). public bool IsTaskBased => TaskId is not null; [ObservableProperty] private string _displayTitle; [ObservableProperty] private InteractiveTerminalViewModel _terminal = new(); /// Set by the host (Mission Control) while a submit-for-review round trip is in /// flight, so a rapid double-click can't race two submissions for the same task. [ObservableProperty] private bool _isSubmitPending; /// Raised when the terminal failed to start — the host surfaces this via the footer error strip. public event Action? ErrorReported; /// Set by the host (Mission Control) to remove this pane from its collection. public Action? CloseRequested { get; set; } /// Raised when the user submits this task's hand-driven work for review; the host /// commits the worktree and moves the task to WaitingForReview. public event Action? SubmitForReviewRequested; /// Task-based pane — dedup'd by . Pass null for an ad-hoc pane /// (no task, never deduped); prefer at ad-hoc call sites. /// is resolved by the pane itself (in ) /// so the tile — and its starting spinner — is visible while the launch spec is still being /// fetched. The host must wire its handlers and then call . public ConPtyPaneViewModel( string? taskId, string displayTitle, Func> descriptorFactory) { TaskId = taskId; _displayTitle = displayTitle; _descriptorFactory = descriptorFactory; Terminal.PropertyChanged += OnTerminalPropertyChanged; } private readonly Func> _descriptorFactory; /// Resolves the launch spec and spawns the session. Call after wiring /// so a failed launch is not swallowed. public void Start() => _ = StartAsync(); private async System.Threading.Tasks.Task StartAsync() { try { Terminal.Start(await _descriptorFactory()); } catch (Exception ex) { Terminal.ReportStartFailure(ex.Message); } } /// Ad-hoc pane — no task, no dedup. public static ConPtyPaneViewModel CreateAdHoc( string displayTitle, Func> descriptorFactory) => new(null, displayTitle, descriptorFactory); private void OnTerminalPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(InteractiveTerminalViewModel.StartError) && Terminal.StartError is { Length: > 0 } error) ErrorReported?.Invoke(error); if (e.PropertyName is nameof(InteractiveTerminalViewModel.IsStarting) or nameof(InteractiveTerminalViewModel.StartError) or nameof(InteractiveTerminalViewModel.HasExited)) { SubmitForReviewCommand.NotifyCanExecuteChanged(); RetryCommand.NotifyCanExecuteChanged(); } } [RelayCommand] private void Close() => CloseRequested?.Invoke(this); private bool CanSubmitForReview() => IsTaskBased && !IsSubmitPending && !Terminal.IsStarting && Terminal.StartError is null && !Terminal.HasExited; [RelayCommand(CanExecute = nameof(CanSubmitForReview))] private void SubmitForReview() { if (TaskId is { } id) SubmitForReviewRequested?.Invoke(id); } partial void OnIsSubmitPendingChanged(bool value) => SubmitForReviewCommand.NotifyCanExecuteChanged(); // A launch failure permanently occupies the TaskId dedupe slot unless the user can retry — // re-opening the same task would otherwise just re-focus a dead tile. private bool CanRetry() => Terminal.HasExited && Terminal.StartError is not null; [RelayCommand(CanExecute = nameof(CanRetry))] private void Retry() { Terminal.PropertyChanged -= OnTerminalPropertyChanged; Terminal.Dispose(); Terminal = new InteractiveTerminalViewModel(); Terminal.PropertyChanged += OnTerminalPropertyChanged; Start(); } public void Dispose() { Terminal.PropertyChanged -= OnTerminalPropertyChanged; Terminal.Kill(); Terminal.Dispose(); } }