feat(ui): merge action and robust jump-to-task in worktrees overview
Add Merge entry to the worktrees overview context menu wiring the existing MergeModalViewModel, replace fire-and-forget list selection with a collection-change-aware JumpToTaskHelper, and propagate list renames to visible task rows via a new ListUpdated event. Harden worktree state changes: WorkerHub.SetWorktreeState now rejects invalid transitions, WorktreeMaintenanceService only drops the DB row when the on-disk worktree was actually removed, and Cleanup/Reset broadcast WorktreeUpdated for affected tasks. SetWorktreeStateAsync returns the hub error message so the modal can surface it. Also: de-duplicate the worktrees overview modal opener, hook OnParentTaskIdChanged to refresh IsDraft, fix MergeModal CanExecute notifications, and add WorktreeStateHubTests for the transition rules. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,15 +15,15 @@ public sealed partial class WorktreeOverviewRowViewModel : ViewModelBase
|
||||
{
|
||||
[ObservableProperty] private string _taskId = "";
|
||||
[ObservableProperty] private string _taskTitle = "";
|
||||
[ObservableProperty] private TaskStatus _taskStatus;
|
||||
[ObservableProperty][NotifyPropertyChangedFor(nameof(IsRunning))] private TaskStatus _taskStatus;
|
||||
[ObservableProperty] private string _listId = "";
|
||||
[ObservableProperty] private string _listName = "";
|
||||
[ObservableProperty] private string _path = "";
|
||||
[ObservableProperty] private string _branchName = "";
|
||||
[ObservableProperty] private string _baseCommit = "";
|
||||
[ObservableProperty] private WorktreeState _state;
|
||||
[ObservableProperty][NotifyPropertyChangedFor(nameof(IsActive))] private WorktreeState _state;
|
||||
[ObservableProperty] private string? _diffStat;
|
||||
[ObservableProperty] private DateTime _createdAt;
|
||||
[ObservableProperty][NotifyPropertyChangedFor(nameof(AgeText))] private DateTime _createdAt;
|
||||
[ObservableProperty] private bool _pathExistsOnDisk;
|
||||
[ObservableProperty] private bool _isSelected;
|
||||
|
||||
@@ -66,6 +66,8 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
|
||||
public Action<WorktreeModalViewModel>? ShowDiffAction { get; set; }
|
||||
public Action<string, string>? JumpToTaskAction { get; set; }
|
||||
public Func<string, Task<bool>>? ConfirmAction { get; set; }
|
||||
public Func<MergeModalViewModel>? ResolveMergeVm { get; set; }
|
||||
public Func<MergeModalViewModel, Task>? ShowMergeAction { get; set; }
|
||||
|
||||
public WorktreesOverviewModalViewModel(WorkerClient worker, Func<WorktreeModalViewModel> diffVmFactory)
|
||||
{
|
||||
@@ -103,7 +105,8 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
|
||||
Groups.Clear();
|
||||
if (IsGlobal)
|
||||
{
|
||||
foreach (var grp in ordered.GroupBy(r => (r.ListId, r.ListName)).OrderBy(g => g.Key.ListName))
|
||||
foreach (var grp in ordered.GroupBy(r => (r.ListId, r.ListName))
|
||||
.OrderBy(g => g.Key.ListName, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var group = new WorktreesGroupViewModel { ListId = grp.Key.ListId, ListName = grp.Key.ListName };
|
||||
foreach (var row in grp) group.Rows.Add(row);
|
||||
@@ -158,10 +161,21 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
|
||||
private void OpenInExplorer(WorktreeOverviewRowViewModel? row)
|
||||
{
|
||||
if (row is null || !row.PathExistsOnDisk) return;
|
||||
try { Process.Start(new ProcessStartInfo { FileName = "explorer.exe", Arguments = $"\"{row.Path}\"", UseShellExecute = true }); }
|
||||
try { Process.Start(new ProcessStartInfo { FileName = row.Path, UseShellExecute = true }); }
|
||||
catch { }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task Merge(WorktreeOverviewRowViewModel? row)
|
||||
{
|
||||
if (row is null || row.State != WorktreeState.Active) return;
|
||||
if (ResolveMergeVm is null || ShowMergeAction is null) return;
|
||||
var mergeVm = ResolveMergeVm();
|
||||
await mergeVm.InitializeAsync(row.TaskId, row.TaskTitle);
|
||||
await ShowMergeAction(mergeVm);
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void JumpToTask(WorktreeOverviewRowViewModel? row)
|
||||
{
|
||||
@@ -174,16 +188,18 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
|
||||
private async Task Discard(WorktreeOverviewRowViewModel? row)
|
||||
{
|
||||
if (row is null || row.State != WorktreeState.Active) return;
|
||||
if (await _worker.SetWorktreeStateAsync(row.TaskId, WorktreeState.Discarded))
|
||||
row.State = WorktreeState.Discarded;
|
||||
var (ok, err) = await _worker.SetWorktreeStateAsync(row.TaskId, WorktreeState.Discarded);
|
||||
if (ok) row.State = WorktreeState.Discarded;
|
||||
else StatusMessage = err ?? "Failed to discard worktree.";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task Keep(WorktreeOverviewRowViewModel? row)
|
||||
{
|
||||
if (row is null || row.State != WorktreeState.Active) return;
|
||||
if (await _worker.SetWorktreeStateAsync(row.TaskId, WorktreeState.Kept))
|
||||
row.State = WorktreeState.Kept;
|
||||
var (ok, err) = await _worker.SetWorktreeStateAsync(row.TaskId, WorktreeState.Kept);
|
||||
if (ok) row.State = WorktreeState.Kept;
|
||||
else StatusMessage = err ?? "Failed to keep worktree.";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
||||
Reference in New Issue
Block a user