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:
mika kuns
2026-05-27 13:43:39 +02:00
parent 2223839595
commit 967e0cd319
18 changed files with 416 additions and 53 deletions

View File

@@ -239,12 +239,16 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
public async Task<WorktreeCleanupDto> CleanupFinishedWorktrees(string? listId = null)
{
var result = await _wtMaintenance.CleanupFinishedAsync(listId, Context.ConnectionAborted);
foreach (var id in result.RemovedTaskIds)
await _broadcaster.WorktreeUpdated(id);
return new WorktreeCleanupDto(result.Removed);
}
public async Task<WorktreeResetDto> ResetAllWorktrees()
{
var result = await _wtMaintenance.ResetAllAsync();
foreach (var id in result.RemovedTaskIds)
await _broadcaster.WorktreeUpdated(id);
return new WorktreeResetDto(result.Removed, result.TasksAffected, result.Blocked, result.RunningTasks);
}
@@ -262,6 +266,12 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
var repo = new WorktreeRepository(ctx);
var existing = await repo.GetByTaskIdAsync(taskId, Context.ConnectionAborted);
if (existing is null) throw new HubException("worktree not found");
// Allowed transitions: Active -> Merged | Discarded | Kept. Terminal states are final.
if (existing.State == newState) return true;
if (existing.State != WorktreeState.Active || newState == WorktreeState.Active)
throw new HubException($"invalid worktree state transition {existing.State} -> {newState}");
await repo.SetStateAsync(taskId, newState, Context.ConnectionAborted);
await _broadcaster.WorktreeUpdated(taskId);
return true;