fix(ui): gate Mission Control submit-for-review, add retry, fix event leak
Submit for Review is now disabled while a ConPTY pane is starting, has failed to launch, or has already exited, and MissionControlViewModel guards against a rapid double-click racing two SubmitTaskForReviewAsync calls. A failed launch no longer permanently occupies its TaskId dedupe slot -- a Retry button re-fetches the launch spec and restarts the pane in place. CloseConPtySession/Dispose now also unsubscribe SubmitForReviewRequested, matching the other pane event handlers. Also fixes the pre-existing nullable-dereference warning in IslandsShellViewModel.SyncInteractiveSessionChips.
This commit is contained in:
@@ -285,6 +285,8 @@
|
||||
"submitForReviewFailed": "Einreichen zum Review fehlgeschlagen: {0}",
|
||||
"submitForReview": "Zum Review einreichen",
|
||||
"submitForReviewTip": "Diesen Worktree committen und den Task ins Review bringen, damit er gemergt werden kann",
|
||||
"retry": "Erneut versuchen",
|
||||
"retryTip": "Diese Sitzung erneut starten",
|
||||
"planningTitleSuffix": " (Planung)",
|
||||
"question": {
|
||||
"title": "Claude fragt nach",
|
||||
|
||||
@@ -285,6 +285,8 @@
|
||||
"submitForReviewFailed": "Couldn't submit for review: {0}",
|
||||
"submitForReview": "Submit for review",
|
||||
"submitForReviewTip": "Commit this worktree and move the task to review so it can be merged",
|
||||
"retry": "Retry",
|
||||
"retryTip": "Try launching this session again",
|
||||
"planningTitleSuffix": " (Planning)",
|
||||
"question": {
|
||||
"title": "Claude is asking",
|
||||
|
||||
@@ -473,7 +473,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
|
||||
private void SyncInteractiveSessionChips()
|
||||
{
|
||||
if (MissionControl is null) return;
|
||||
if (MissionControl is null || Tasks is null) return;
|
||||
Tasks.SyncInteractiveSessions(
|
||||
MissionControl.ConPtySessions
|
||||
.Select(s => s.TaskId)
|
||||
|
||||
@@ -20,7 +20,11 @@ public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControl
|
||||
|
||||
[ObservableProperty] private string _displayTitle;
|
||||
|
||||
public InteractiveTerminalViewModel Terminal { get; } = new();
|
||||
[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>Raised when the terminal failed to start — the host surfaces this via the footer error strip.</summary>
|
||||
public event Action<string>? ErrorReported;
|
||||
@@ -76,12 +80,21 @@ public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControl
|
||||
{
|
||||
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;
|
||||
private bool CanSubmitForReview() =>
|
||||
IsTaskBased && !IsSubmitPending && !Terminal.IsStarting && Terminal.StartError is null && !Terminal.HasExited;
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanSubmitForReview))]
|
||||
private void SubmitForReview()
|
||||
@@ -89,6 +102,22 @@ public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControl
|
||||
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;
|
||||
|
||||
@@ -302,16 +302,22 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
|
||||
// Submit a task's hand-driven ConPTY work for review, then close the pane (the interactive
|
||||
// session is finished). The worker commits the worktree and moves the task to WaitingForReview.
|
||||
// Guarded by the pane's IsSubmitPending flag — a rapid double-click would otherwise race two
|
||||
// SubmitTaskForReviewAsync calls, with the loser flashing a spurious footer error.
|
||||
private async void OnPaneSubmitForReview(string taskId)
|
||||
{
|
||||
if (ConPtySessions.FirstOrDefault(s => s.TaskId == taskId) is not { } pane || pane.IsSubmitPending)
|
||||
return;
|
||||
|
||||
pane.IsSubmitPending = true;
|
||||
try
|
||||
{
|
||||
await _worker.SubmitTaskForReviewAsync(taskId);
|
||||
if (ConPtySessions.FirstOrDefault(s => s.TaskId == taskId) is { } pane)
|
||||
CloseConPtySession(pane);
|
||||
CloseConPtySession(pane);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
pane.IsSubmitPending = false;
|
||||
ErrorReported?.Invoke(Loc.T("missionControl.submitForReviewFailed", ex.Message));
|
||||
}
|
||||
}
|
||||
@@ -321,6 +327,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
if (!ConPtySessions.Contains(pane)) return;
|
||||
pane.ErrorReported -= OnConPtyPaneError;
|
||||
pane.CloseRequested -= CloseConPtySession;
|
||||
pane.SubmitForReviewRequested -= OnPaneSubmitForReview;
|
||||
ConPtySessions.Remove(pane);
|
||||
pane.Dispose();
|
||||
}
|
||||
@@ -370,6 +377,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
{
|
||||
c.ErrorReported -= OnConPtyPaneError;
|
||||
c.CloseRequested -= CloseConPtySession;
|
||||
c.SubmitForReviewRequested -= OnPaneSubmitForReview;
|
||||
c.Dispose();
|
||||
}
|
||||
ConPtySessions.Clear();
|
||||
|
||||
@@ -39,9 +39,15 @@
|
||||
Background="{DynamicResource ErrorTintBrush}"
|
||||
BorderBrush="{DynamicResource BloodBrush}"
|
||||
BorderThickness="0,0,0,1" Padding="12,6">
|
||||
<TextBlock Classes="meta" Text="{Binding Terminal.StartError}"
|
||||
Foreground="{DynamicResource BloodBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="meta" Text="{Binding Terminal.StartError}"
|
||||
Foreground="{DynamicResource BloodBrush}"
|
||||
TextWrapping="Wrap" VerticalAlignment="Center" />
|
||||
<Button Grid.Column="1" Classes="btn" Margin="12,0,0,0"
|
||||
Content="{loc:Tr missionControl.retry}"
|
||||
ToolTip.Tip="{loc:Tr missionControl.retryTip}"
|
||||
Command="{Binding RetryCommand}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Embedded ConPTY terminal, with a starting overlay until the session is spawned -->
|
||||
|
||||
Reference in New Issue
Block a user