Ein "Abandon run"-Knopf an Handler-Panes fragt HandlerRoundState.Get(handlerTaskId) nach den noch offenen (Queued/Running) Tasks des Laufs, bestaetigt mit Anzahl, cancelt jeden ueber den bestehenden CancelTaskAsync-Pfad und schliesst danach die Session -- ein Schritt statt Tile schliessen + Queue separat abbrechen. Ohne offene Tasks (oder ohne Round-Eintrag) schliesst die Aktion direkt, ohne Dialog. Nur an den beiden Handler-Panes sichtbar (IsListHandler), normale Task-/Ad-hoc- Panes bleiben unveraendert.
190 lines
8.2 KiB
C#
190 lines
8.2 KiB
C#
using System;
|
|
using System.ComponentModel;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using ClaudeDo.Ui.Services;
|
|
|
|
namespace ClaudeDo.Ui.ViewModels.MissionControl;
|
|
|
|
/// <summary>
|
|
/// 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, <see cref="TaskId"/>
|
|
/// is null — ad-hoc panes are never deduped, unlike task-based ones).
|
|
/// </summary>
|
|
public sealed partial class ConPtyPaneViewModel : ViewModelBase, 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;
|
|
|
|
/// <summary>Set by the host for a list-handler run ("Let Claude handle it" / "Let Claude merge
|
|
/// the reviews") pane -- shows the "Abandon run" action. Plain mutable property, not
|
|
/// <see cref="ObservableProperty"/>: the host sets it once right after construction, before the
|
|
/// pane is added to the visible collection, so no change notification is needed.</summary>
|
|
public bool IsListHandler { get; set; }
|
|
|
|
[ObservableProperty] private string _displayTitle;
|
|
|
|
[ObservableProperty] private InteractiveTerminalViewModel _terminal = new();
|
|
|
|
/// <summary>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.</summary>
|
|
[ObservableProperty] private bool _isSubmitPending;
|
|
|
|
/// <summary>Set by the host while an abandon-run round trip (query open tasks, confirm,
|
|
/// cancel each, close pane) is in flight, so a rapid double-click can't race two runs.</summary>
|
|
[ObservableProperty] private bool _isAbandonPending;
|
|
|
|
/// <summary>Raised when the terminal failed to start — the host surfaces this via the footer error strip.</summary>
|
|
public event Action<string>? ErrorReported;
|
|
|
|
/// <summary>Set by the host (Mission Control) to remove this pane from its collection.</summary>
|
|
public Action<ConPtyPaneViewModel>? CloseRequested { get; set; }
|
|
|
|
/// <summary>Raised when the user submits this task's hand-driven work for review; the host
|
|
/// commits the worktree and moves the task to WaitingForReview.</summary>
|
|
public event Action<string>? SubmitForReviewRequested;
|
|
|
|
/// <summary>Raised when the user abandons a list-handler run (<see cref="IsListHandler"/>
|
|
/// only): the host looks up this round's still-open tasks, confirms with the user (naming the
|
|
/// count), cancels them, then closes this pane -- one action instead of closing the tile and
|
|
/// separately cancelling the queue.</summary>
|
|
public event Action<string>? AbandonRunRequested;
|
|
|
|
/// <summary>Raised with <c>(taskId, exitCode)</c> when a task-based session that had been running
|
|
/// dies non-zero. The host reports it to the worker, which decides whether this was a list
|
|
/// handler killed by the usage limit and worth reopening. Ad-hoc panes never raise it — there is
|
|
/// no task to reopen.</summary>
|
|
public event Action<string, int>? SessionDiedAfterStartup;
|
|
|
|
/// <summary>Task-based pane — dedup'd by <see cref="TaskId"/>. Pass null for an ad-hoc pane
|
|
/// (no task, never deduped); prefer <see cref="CreateAdHoc"/> at ad-hoc call sites.
|
|
/// <paramref name="descriptorFactory"/> is resolved by the pane itself (in <see cref="Start"/>)
|
|
/// 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 <see cref="Start"/>.</summary>
|
|
public ConPtyPaneViewModel(
|
|
string? taskId,
|
|
string displayTitle,
|
|
Func<System.Threading.Tasks.Task<TerminalLaunchDescriptor>> descriptorFactory)
|
|
{
|
|
TaskId = taskId;
|
|
_displayTitle = displayTitle;
|
|
_descriptorFactory = descriptorFactory;
|
|
Subscribe(Terminal);
|
|
}
|
|
|
|
// Terminal is swapped wholesale on Retry, so the handlers follow the property rather than being
|
|
// wired once at construction. (The initial value comes from the field initializer, which never
|
|
// runs through the setter -- hence the explicit Subscribe in the constructor.)
|
|
partial void OnTerminalChanging(InteractiveTerminalViewModel? oldValue, InteractiveTerminalViewModel newValue)
|
|
{
|
|
if (oldValue is not null) Unsubscribe(oldValue);
|
|
}
|
|
|
|
partial void OnTerminalChanged(InteractiveTerminalViewModel value) => Subscribe(value);
|
|
|
|
private void Subscribe(InteractiveTerminalViewModel terminal)
|
|
{
|
|
terminal.PropertyChanged += OnTerminalPropertyChanged;
|
|
terminal.ExitedAfterStartup += OnTerminalExitedAfterStartup;
|
|
}
|
|
|
|
private void Unsubscribe(InteractiveTerminalViewModel terminal)
|
|
{
|
|
terminal.PropertyChanged -= OnTerminalPropertyChanged;
|
|
terminal.ExitedAfterStartup -= OnTerminalExitedAfterStartup;
|
|
}
|
|
|
|
private void OnTerminalExitedAfterStartup(int exitCode)
|
|
{
|
|
if (TaskId is { } id) SessionDiedAfterStartup?.Invoke(id, exitCode);
|
|
}
|
|
|
|
private readonly Func<System.Threading.Tasks.Task<TerminalLaunchDescriptor>> _descriptorFactory;
|
|
|
|
/// <summary>Resolves the launch spec and spawns the session. Call after wiring
|
|
/// <see cref="ErrorReported"/> so a failed launch is not swallowed.</summary>
|
|
public void Start() => _ = StartAsync();
|
|
|
|
private async System.Threading.Tasks.Task StartAsync()
|
|
{
|
|
try
|
|
{
|
|
Terminal.Start(await _descriptorFactory());
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Terminal.ReportStartFailure(ex.Message);
|
|
}
|
|
}
|
|
|
|
/// <summary>Ad-hoc pane — no task, no dedup.</summary>
|
|
public static ConPtyPaneViewModel CreateAdHoc(
|
|
string displayTitle,
|
|
Func<System.Threading.Tasks.Task<TerminalLaunchDescriptor>> 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();
|
|
}
|
|
}
|
|
|
|
/// <summary>Types a message into this pane's live session and submits it (the worker's
|
|
/// pre-usage-limit warning). False when the session isn't running.</summary>
|
|
public System.Threading.Tasks.Task<bool> SendLineAsync(string text) => Terminal.SendLineAsync(text);
|
|
|
|
[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();
|
|
|
|
private bool CanAbandonRun() => IsListHandler && !IsAbandonPending;
|
|
|
|
[RelayCommand(CanExecute = nameof(CanAbandonRun))]
|
|
private void AbandonRun()
|
|
{
|
|
if (TaskId is { } id) AbandonRunRequested?.Invoke(id);
|
|
}
|
|
|
|
partial void OnIsAbandonPendingChanged(bool value) => AbandonRunCommand.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()
|
|
{
|
|
var dead = Terminal;
|
|
Terminal = new InteractiveTerminalViewModel(); // OnTerminalChanging/Changed move the handlers
|
|
dead.Dispose();
|
|
Start();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Unsubscribe(Terminal);
|
|
Terminal.Kill();
|
|
Terminal.Dispose();
|
|
}
|
|
}
|