Sechs Konsistenz-Fixes: Delete-Task ins Zeilen-Kontextmenü (routet über den Worker wie DetailsIslandViewModel.DeleteTaskAsync, damit ein gelöschtes Child den WaitingForChildren-Parent korrekt weiterschaltet); EnqueueCommand als "Send to queue"-Button im Detail-Pane verdrahtet; Settings-Eintrag im Listen-Kontextmenü ergänzt; Worktree-Discard und Reset-All laufen jetzt über das jeweils vorhandene Confirm-Hook (ConfirmAction / neuer ConfirmAsync-Hook auf WorktreesSettingsTabViewModel) statt ohne Rückfrage bzw. über ein Inline-Reveal-Banner; killSessionTip/closeSession und die deutschen Queue-Strings im usageMonitor vereinheitlicht. Kein zweiter Dialog-Mechanismus eingeführt — überall die vorhandenen Hooks wiederverwendet.
198 lines
8.5 KiB
C#
198 lines
8.5 KiB
C#
using System.Linq;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.VisualTree;
|
|
using ClaudeDo.Data.Models;
|
|
using ClaudeDo.Ui.Localization;
|
|
using ClaudeDo.Ui.ViewModels.Islands;
|
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
|
|
|
namespace ClaudeDo.Ui.Views.Islands;
|
|
|
|
public partial class TaskRowView : UserControl
|
|
{
|
|
private TaskRowViewModel? _pendingScheduleRow;
|
|
|
|
public TaskRowView() { InitializeComponent(); }
|
|
|
|
private TasksIslandViewModel? FindTasksVm() =>
|
|
this.GetVisualAncestors().OfType<ItemsControl>()
|
|
.Select(ic => ic.DataContext).OfType<TasksIslandViewModel>().FirstOrDefault();
|
|
|
|
// The context menu is built here instead of declared in XAML so the ~18 MenuItems only
|
|
// exist while a row's menu is actually open, not for every row rendered in the list.
|
|
private void OnRowContextRequested(object? sender, ContextRequestedEventArgs e)
|
|
{
|
|
if (sender is not Border border) return;
|
|
if (DataContext is not TaskRowViewModel row) return;
|
|
if (FindTasksVm() is not { } vm) return;
|
|
|
|
// OnTunnelPointerPressed (TasksIslandView) only selects on the left button, so
|
|
// right-click needs its own explicit selection before the menu opens.
|
|
vm.SelectFrom(row, "context-menu");
|
|
|
|
var menu = new ContextMenu { DataContext = row };
|
|
|
|
MenuItem MakeItem(string key, EventHandler<RoutedEventArgs> click, bool isVisible = true, string? tag = null)
|
|
{
|
|
var item = new MenuItem { Header = Loc.T(key), DataContext = row, IsVisible = isVisible };
|
|
if (tag is not null) item.Tag = tag;
|
|
item.Click += click;
|
|
return item;
|
|
}
|
|
|
|
menu.Items.Add(MakeItem("tasks.ctxSendToQueue", OnSendToQueueClick, row.CanSendToQueue));
|
|
menu.Items.Add(MakeItem("tasks.ctxRemoveFromQueue", OnRemoveFromQueueClick, row.CanRemoveFromQueue));
|
|
menu.Items.Add(MakeItem("tasks.ctxCancelExecution", OnCancelExecutionClick, row.IsRunning));
|
|
menu.Items.Add(new Separator());
|
|
|
|
var markAs = new MenuItem { Header = Loc.T("tasks.ctxMarkAs"), DataContext = row };
|
|
markAs.Items.Add(MakeItem("tasks.ctxMarkDone", OnSetStatusClick, tag: "Done"));
|
|
markAs.Items.Add(MakeItem("tasks.ctxMarkCancelled", OnSetStatusClick, tag: "Cancelled"));
|
|
markAs.Items.Add(new Separator());
|
|
markAs.Items.Add(MakeItem("tasks.ctxMarkManual", OnToggleManualClick, !row.IsManual));
|
|
markAs.Items.Add(MakeItem("tasks.ctxMarkClaudeTask", OnToggleManualClick, row.IsManual));
|
|
menu.Items.Add(markAs);
|
|
|
|
menu.Items.Add(new Separator());
|
|
menu.Items.Add(MakeItem("tasks.ctxOpenConPtySession", OnOpenConPtySessionClick));
|
|
menu.Items.Add(MakeItem("tasks.ctxOpenPlanningSession", OnOpenPlanningSessionClick, row.CanOpenPlanningSession));
|
|
menu.Items.Add(MakeItem("tasks.ctxResumePlanningSession", OnResumePlanningSessionClick, row.CanResumeOrDiscardPlanning));
|
|
menu.Items.Add(MakeItem("tasks.ctxFinalizePlanningSession", OnFinalizePlanningSessionClick, row.CanFinalizePlanning));
|
|
menu.Items.Add(MakeItem("tasks.ctxDiscardPlanningSession", OnDiscardPlanningSessionClick, row.CanResumeOrDiscardPlanning));
|
|
menu.Items.Add(new Separator());
|
|
menu.Items.Add(MakeItem("tasks.ctxScheduleFor", OnScheduleForClick));
|
|
menu.Items.Add(MakeItem("tasks.ctxClearSchedule", OnClearScheduleClick, row.HasSchedule));
|
|
menu.Items.Add(MakeItem("tasks.ctxAddToMyDay", OnAddToMyDayClick, row.CanAddToMyDay));
|
|
menu.Items.Add(MakeItem("tasks.ctxRemoveFromMyDay", OnRemoveFromMyDayClick, row.IsMyDay));
|
|
menu.Items.Add(new Separator());
|
|
menu.Items.Add(MakeItem("tasks.ctxDeleteTask", OnDeleteTaskClick));
|
|
|
|
menu.Open(border);
|
|
e.Handled = true;
|
|
}
|
|
|
|
private async void OnSendToQueueClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
await vm.SendToQueueCommand.ExecuteAsync(row);
|
|
}
|
|
|
|
private async void OnRemoveFromQueueClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
await vm.RemoveFromQueueCommand.ExecuteAsync(row);
|
|
}
|
|
|
|
private async void OnCancelExecutionClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
await vm.CancelRunningTaskCommand.ExecuteAsync(row);
|
|
}
|
|
|
|
private async void OnClearScheduleClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
await vm.ClearScheduleCommand.ExecuteAsync(row);
|
|
}
|
|
|
|
private async void OnToggleManualClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
await vm.ToggleManualCommand.ExecuteAsync(row);
|
|
}
|
|
|
|
private async void OnAddToMyDayClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
await vm.AddToMyDayCommand.ExecuteAsync(row);
|
|
}
|
|
|
|
private async void OnRemoveFromMyDayClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
await vm.RemoveFromMyDayCommand.ExecuteAsync(row);
|
|
}
|
|
|
|
private async void OnDeleteTaskClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
await vm.DeleteTaskCommand.ExecuteAsync(row);
|
|
}
|
|
|
|
private void OnOpenPlanningSessionClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
vm.OpenPlanningSessionCommand.Execute(row);
|
|
}
|
|
|
|
private void OnOpenConPtySessionClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
vm.OpenConPtySessionCommand.Execute(row);
|
|
}
|
|
|
|
// The status chip is only actionable while a ConPTY session is open; the open command dedupes
|
|
// by task id, so it surfaces Mission Control and focuses the existing pane.
|
|
private void OnStatusChipTapped(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is not TaskRowViewModel { HasInteractiveSession: true } row) return;
|
|
if (FindTasksVm() is { } vm)
|
|
vm.OpenConPtySessionCommand.Execute(row);
|
|
}
|
|
|
|
private async void OnResumePlanningSessionClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
await vm.ResumePlanningSessionCommand.ExecuteAsync(row);
|
|
}
|
|
|
|
private async void OnDiscardPlanningSessionClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
await vm.DiscardPlanningSessionCommand.ExecuteAsync(row);
|
|
}
|
|
|
|
private async void OnFinalizePlanningSessionClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
await vm.FinalizePlanningSessionCommand.ExecuteAsync(row);
|
|
}
|
|
|
|
private async void OnSetStatusClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (sender is not MenuItem mi) return;
|
|
if (DataContext is not TaskRowViewModel row) return;
|
|
if (FindTasksVm() is not { } vm) return;
|
|
if (mi.Tag is not string tag) return;
|
|
if (!System.Enum.TryParse<TaskStatus>(tag, ignoreCase: true, out var status)) return;
|
|
await vm.SetStatusOnRowAsync(row, status);
|
|
}
|
|
|
|
private void OnScheduleForClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is not TaskRowViewModel row) return;
|
|
_pendingScheduleRow = row;
|
|
ScheduleDate.SelectedDate = row.ScheduledFor ?? DateTime.Now.AddHours(1);
|
|
ScheduleAnchor.Flyout?.ShowAt(ScheduleAnchor);
|
|
}
|
|
|
|
private async void OnScheduleSetClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
ScheduleAnchor.Flyout?.Hide();
|
|
if (_pendingScheduleRow is null || ScheduleDate.SelectedDate is null) return;
|
|
var when = ScheduleDate.SelectedDate.Value;
|
|
if (FindTasksVm() is { } tvm)
|
|
await tvm.SetScheduledForAsync(_pendingScheduleRow, when);
|
|
_pendingScheduleRow = null;
|
|
}
|
|
|
|
private void OnScheduleCancelClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
ScheduleAnchor.Flyout?.Hide();
|
|
_pendingScheduleRow = null;
|
|
}
|
|
}
|