fix(claude-do): merge Task-Delete mit offener ConPTY-Session blocken + Ghost-Selec

ClaudeDo-Task: df60c0ca-40e5-4284-b887-1cb45244947c
This commit is contained in:
Mika Kuns
2026-08-26 15:47:20 +02:00
6 changed files with 159 additions and 5 deletions
@@ -1002,7 +1002,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
private void OnBoundTaskPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(TaskRowViewModel.HasInteractiveSession))
{
ResetAndRetryCommand.NotifyCanExecuteChanged();
DeleteTaskCommand.NotifyCanExecuteChanged();
}
}
[RelayCommand]
@@ -1099,7 +1102,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
CloseDetail?.Invoke();
}
private bool CanDeleteTask() => Task != null && _worker.IsConnected;
private bool CanDeleteTask() => Task != null && _worker.IsConnected && !Task.HasInteractiveSession;
[RelayCommand]
private async System.Threading.Tasks.Task CommitSubtaskEditAsync(SubtaskRowViewModel? row)
@@ -219,6 +219,13 @@ public sealed partial class TaskRowViewModel : ViewModelBase
}
public string? PlanningDisabledReason => PlanningGate().Reason;
// Deleting out from under a live ConPTY session would orphan the claude process in Mission
// Control (it never gets a task-deleted signal) — same rationale as SendToQueueGate.
private (bool Can, string? Reason) DeleteGate() =>
HasInteractiveSession ? (false, Loc.T("tasks.reasonInteractiveSession")) : (true, null);
public bool CanDeleteTask => DeleteGate().Can;
public string? DeleteDisabledReason => DeleteGate().Reason;
public bool HasSchedule => ScheduledFor.HasValue;
// "Add to My Day" — shown on any task not already in My Day; a Done task has no place in
// today's focus list. The mirror of "Remove from My Day" (gated on IsMyDay).
@@ -402,6 +409,8 @@ public sealed partial class TaskRowViewModel : ViewModelBase
OnPropertyChanged(nameof(StatusChipTooltip));
OnPropertyChanged(nameof(CanSendToQueue));
OnPropertyChanged(nameof(SendToQueueDisabledReason));
OnPropertyChanged(nameof(CanDeleteTask));
OnPropertyChanged(nameof(DeleteDisabledReason));
}
partial void OnFailureReasonChanged(string? value)
@@ -311,20 +311,28 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
}
var existing = Items.FirstOrDefault(r => r.Id == taskId);
var removed = false;
if (entity is null)
{
if (existing is not null) Items.Remove(existing);
if (existing is not null) { Items.Remove(existing); removed = true; }
}
else
{
var matches = TaskMatchesList(entity, list);
if (existing is not null && matches) existing.UpdateFromEntity(entity);
else if (existing is not null) Items.Remove(existing);
else if (existing is not null) { Items.Remove(existing); removed = true; }
else if (matches) { LoadForList(list); return; }
else return;
}
// The row is gone from Items but SelectedTask (and the bound detail pane) would still
// point at it — happens when a task is deleted externally (MCP/another session) while
// selected here, since that path never runs SelectFrom(null, ...) like the in-UI delete
// does.
if (removed && SelectedTask == existing)
SelectFrom(null, "row-removed");
// Keep the parent's HasQueuedSubtasks flag in sync when a child's status flips.
if (entity is not null && !string.IsNullOrEmpty(entity.ParentTaskId))
{
@@ -1191,7 +1199,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
[RelayCommand]
private async Task DeleteTaskAsync(TaskRowViewModel? row)
{
if (row is null || _worker is null) return;
if (row is null || row.HasInteractiveSession || _worker is null) return;
if (ConfirmAsync is not null)
{
var ok = await ConfirmAsync(Loc.T("vm.tasksIsland.deleteTaskConfirm", row.Title));
@@ -1216,6 +1224,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
}
Items.Remove(row);
if (SelectedTask == row) SelectFrom(null, "row-removed");
Regroup();
UpdateSubtitle();
TasksChanged?.Invoke(this, EventArgs.Empty);
@@ -98,7 +98,7 @@ public partial class TaskRowView : UserControl
menu.Items.Add(schedule);
menu.Items.Add(new Separator());
menu.Items.Add(MakeItem("tasks.ctxDeleteTask", OnDeleteTaskClick));
menu.Items.Add(MakeItem("tasks.ctxDeleteTask", OnDeleteTaskClick, reason: row.DeleteDisabledReason));
menu.Open(border);
e.Handled = true;