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). Distinct from the streamed-log /// pane; the two coexist until /// the streaming interactive stack is removed. /// 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; public InteractiveTerminalViewModel Terminal { get; } = new(); /// 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); } [RelayCommand] private void Close() => CloseRequested?.Invoke(this); private bool CanSubmitForReview() => IsTaskBased; [RelayCommand(CanExecute = nameof(CanSubmitForReview))] private void SubmitForReview() { if (TaskId is { } id) SubmitForReviewRequested?.Invoke(id); } public void Dispose() { Terminal.PropertyChanged -= OnTerminalPropertyChanged; Terminal.Kill(); Terminal.Dispose(); } }