feat(ui): wire merge-aware approve and preview into the worker client

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-04 23:32:12 +02:00
parent 43f8f7f7d8
commit 3202c76674
7 changed files with 19 additions and 11 deletions

View File

@@ -37,7 +37,9 @@ public interface IWorkerClient : INotifyPropertyChanged
Task<ListConfigDto?> GetListConfigAsync(string listId); Task<ListConfigDto?> GetListConfigAsync(string listId);
Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto); Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto);
Task SetTaskStatusAsync(string taskId, TaskStatus status); Task SetTaskStatusAsync(string taskId, TaskStatus status);
Task ApproveReviewAsync(string taskId); Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch);
Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch);
Task<MergeResultDto> MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage);
Task RejectReviewToQueueAsync(string taskId, string feedback); Task RejectReviewToQueueAsync(string taskId, string feedback);
Task RejectReviewToIdleAsync(string taskId); Task RejectReviewToIdleAsync(string taskId);
Task CancelReviewAsync(string taskId); Task CancelReviewAsync(string taskId);

View File

@@ -396,10 +396,11 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
await _hub.InvokeAsync("SetTaskStatus", taskId, status.ToString()); await _hub.InvokeAsync("SetTaskStatus", taskId, status.ToString());
} }
public async Task ApproveReviewAsync(string taskId) public Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch)
{ => TryInvokeAsync<MergeResultDto>("ApproveReview", taskId, targetBranch);
await _hub.InvokeAsync("ApproveReview", taskId);
} public Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch)
=> TryInvokeAsync<MergePreviewDto>("PreviewMerge", taskId, targetBranch);
public async Task RejectReviewToQueueAsync(string taskId, string feedback) public async Task RejectReviewToQueueAsync(string taskId, string feedback)
{ {
@@ -529,6 +530,7 @@ public sealed record AppSettingsDto(
public sealed record WorktreeCleanupDto(int Removed); public sealed record WorktreeCleanupDto(int Removed);
public sealed record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks); public sealed record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks);
public record MergeResultDto(string Status, IReadOnlyList<string> ConflictFiles, string? ErrorMessage); public record MergeResultDto(string Status, IReadOnlyList<string> ConflictFiles, string? ErrorMessage);
public record MergePreviewDto(string Status, IReadOnlyList<string> ConflictFiles, int ChangedFileCount);
public record MergeTargetsDto(string DefaultBranch, IReadOnlyList<string> LocalBranches); public record MergeTargetsDto(string DefaultBranch, IReadOnlyList<string> LocalBranches);
public sealed record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType); public sealed record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType);
public sealed record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null); public sealed record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);

View File

@@ -1365,7 +1365,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
// The hub rejects (HubException) if the task is no longer WaitingForReview // The hub rejects (HubException) if the task is no longer WaitingForReview
// — e.g. after "Merge all" folded the parent. Swallow it; the TaskUpdated // — e.g. after "Merge all" folded the parent. Swallow it; the TaskUpdated
// broadcast reconciles the UI. An unhandled command exception would crash. // broadcast reconciles the UI. An unhandled command exception would crash.
try { await _worker.ApproveReviewAsync(Task.Id); } try { await _worker.ApproveReviewAsync(Task.Id, SelectedMergeTarget ?? ""); }
catch { /* stale review action; broadcast reconciles */ } catch { /* stale review action; broadcast reconciles */ }
} }

View File

@@ -647,7 +647,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
private async Task ApproveReviewAsync(TaskRowViewModel? row) private async Task ApproveReviewAsync(TaskRowViewModel? row)
{ {
if (row is null || !row.IsWaitingForReview || _worker is null) return; if (row is null || !row.IsWaitingForReview || _worker is null) return;
try { await _worker.ApproveReviewAsync(row.Id); } try { await _worker.ApproveReviewAsync(row.Id, ""); }
catch { /* offline; broadcast reconciles on return */ } catch { /* offline; broadcast reconciles on return */ }
} }

View File

@@ -52,7 +52,9 @@ public abstract class StubWorkerClient : IWorkerClient
public virtual Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null); public virtual Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null);
public virtual Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask; public virtual Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
public virtual Task SetTaskStatusAsync(string taskId, TaskStatus status) => Task.CompletedTask; public virtual Task SetTaskStatusAsync(string taskId, TaskStatus status) => Task.CompletedTask;
public virtual Task ApproveReviewAsync(string taskId) => Task.CompletedTask; public virtual Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) => Task.FromResult<MergeResultDto?>(null);
public virtual Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch) => Task.FromResult<MergePreviewDto?>(null);
public virtual Task<MergeResultDto> MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty<string>(), null));
public virtual Task RejectReviewToQueueAsync(string taskId, string feedback) => Task.CompletedTask; public virtual Task RejectReviewToQueueAsync(string taskId, string feedback) => Task.CompletedTask;
public virtual Task RejectReviewToIdleAsync(string taskId) => Task.CompletedTask; public virtual Task RejectReviewToIdleAsync(string taskId) => Task.CompletedTask;
public virtual Task CancelReviewAsync(string taskId) => Task.CompletedTask; public virtual Task CancelReviewAsync(string taskId) => Task.CompletedTask;

View File

@@ -74,8 +74,8 @@ public class DetailsIslandPlanningTests : IDisposable
private sealed class ThrowingReviewWorkerClient : StubWorkerClient private sealed class ThrowingReviewWorkerClient : StubWorkerClient
{ {
public override bool IsConnected => true; public override bool IsConnected => true;
public override Task ApproveReviewAsync(string taskId) => public override Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) =>
Task.FromException(new InvalidOperationException("Task is not waiting for review; cannot approve.")); Task.FromException<MergeResultDto?>(new InvalidOperationException("Task is not waiting for review; cannot approve."));
} }
private static SubtaskRowViewModel MakeSubtask(TaskStatus status, WorktreeState wt = WorktreeState.Active) => private static SubtaskRowViewModel MakeSubtask(TaskStatus status, WorktreeState wt = WorktreeState.Active) =>

View File

@@ -42,7 +42,9 @@ sealed class FakeWorkerClient : IWorkerClient
public Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null); public Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null);
public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask; public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
public Task SetTaskStatusAsync(string taskId, TaskStatus status) => Task.CompletedTask; public Task SetTaskStatusAsync(string taskId, TaskStatus status) => Task.CompletedTask;
public Task ApproveReviewAsync(string taskId) => Task.CompletedTask; public Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) => Task.FromResult<MergeResultDto?>(null);
public Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch) => Task.FromResult<MergePreviewDto?>(null);
public Task<MergeResultDto> MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty<string>(), null));
public Task RejectReviewToQueueAsync(string taskId, string feedback) => Task.CompletedTask; public Task RejectReviewToQueueAsync(string taskId, string feedback) => Task.CompletedTask;
public Task RejectReviewToIdleAsync(string taskId) => Task.CompletedTask; public Task RejectReviewToIdleAsync(string taskId) => Task.CompletedTask;
public Task CancelReviewAsync(string taskId) => Task.CompletedTask; public Task CancelReviewAsync(string taskId) => Task.CompletedTask;