From 3202c766740044f38ee6ae9a6187d1f437581f26 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 4 Jun 2026 23:32:12 +0200 Subject: [PATCH] feat(ui): wire merge-aware approve and preview into the worker client Co-Authored-By: Claude Sonnet 4.6 --- src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs | 4 +++- src/ClaudeDo.Ui/Services/WorkerClient.cs | 10 ++++++---- .../ViewModels/Islands/DetailsIslandViewModel.cs | 2 +- .../ViewModels/Islands/TasksIslandViewModel.cs | 2 +- tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs | 4 +++- .../ViewModels/DetailsIslandPlanningTests.cs | 4 ++-- .../UiVm/TasksIslandViewModelPlanningTests.cs | 4 +++- 7 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs index ecc319e..b68ed1d 100644 --- a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs @@ -37,7 +37,9 @@ public interface IWorkerClient : INotifyPropertyChanged Task GetListConfigAsync(string listId); Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto); Task SetTaskStatusAsync(string taskId, TaskStatus status); - Task ApproveReviewAsync(string taskId); + Task ApproveReviewAsync(string taskId, string targetBranch); + Task PreviewMergeAsync(string taskId, string targetBranch); + Task MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage); Task RejectReviewToQueueAsync(string taskId, string feedback); Task RejectReviewToIdleAsync(string taskId); Task CancelReviewAsync(string taskId); diff --git a/src/ClaudeDo.Ui/Services/WorkerClient.cs b/src/ClaudeDo.Ui/Services/WorkerClient.cs index f9f8b7f..cc366e5 100644 --- a/src/ClaudeDo.Ui/Services/WorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/WorkerClient.cs @@ -396,10 +396,11 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC await _hub.InvokeAsync("SetTaskStatus", taskId, status.ToString()); } - public async Task ApproveReviewAsync(string taskId) - { - await _hub.InvokeAsync("ApproveReview", taskId); - } + public Task ApproveReviewAsync(string taskId, string targetBranch) + => TryInvokeAsync("ApproveReview", taskId, targetBranch); + + public Task PreviewMergeAsync(string taskId, string targetBranch) + => TryInvokeAsync("PreviewMerge", taskId, targetBranch); 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 WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks); public record MergeResultDto(string Status, IReadOnlyList ConflictFiles, string? ErrorMessage); +public record MergePreviewDto(string Status, IReadOnlyList ConflictFiles, int ChangedFileCount); public record MergeTargetsDto(string DefaultBranch, IReadOnlyList LocalBranches); 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); diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs index babbc2d..d2052d7 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs @@ -1365,7 +1365,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase // The hub rejects (HubException) if the task is no longer WaitingForReview // — e.g. after "Merge all" folded the parent. Swallow it; the TaskUpdated // 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 */ } } diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs index a042289..015b641 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs @@ -647,7 +647,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase private async Task ApproveReviewAsync(TaskRowViewModel? row) { 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 */ } } diff --git a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs index 6240da4..c8919ad 100644 --- a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs +++ b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs @@ -52,7 +52,9 @@ public abstract class StubWorkerClient : IWorkerClient public virtual Task GetListConfigAsync(string listId) => Task.FromResult(null); public virtual Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask; public virtual Task SetTaskStatusAsync(string taskId, TaskStatus status) => Task.CompletedTask; - public virtual Task ApproveReviewAsync(string taskId) => Task.CompletedTask; + public virtual Task ApproveReviewAsync(string taskId, string targetBranch) => Task.FromResult(null); + public virtual Task PreviewMergeAsync(string taskId, string targetBranch) => Task.FromResult(null); + public virtual Task MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty(), null)); public virtual Task RejectReviewToQueueAsync(string taskId, string feedback) => Task.CompletedTask; public virtual Task RejectReviewToIdleAsync(string taskId) => Task.CompletedTask; public virtual Task CancelReviewAsync(string taskId) => Task.CompletedTask; diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs index 6f00e17..c1f9c0d 100644 --- a/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs @@ -74,8 +74,8 @@ public class DetailsIslandPlanningTests : IDisposable private sealed class ThrowingReviewWorkerClient : StubWorkerClient { public override bool IsConnected => true; - public override Task ApproveReviewAsync(string taskId) => - Task.FromException(new InvalidOperationException("Task is not waiting for review; cannot approve.")); + public override Task ApproveReviewAsync(string taskId, string targetBranch) => + Task.FromException(new InvalidOperationException("Task is not waiting for review; cannot approve.")); } private static SubtaskRowViewModel MakeSubtask(TaskStatus status, WorktreeState wt = WorktreeState.Active) => diff --git a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs index c9cfe24..334cf17 100644 --- a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs +++ b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs @@ -42,7 +42,9 @@ sealed class FakeWorkerClient : IWorkerClient public Task GetListConfigAsync(string listId) => Task.FromResult(null); public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask; public Task SetTaskStatusAsync(string taskId, TaskStatus status) => Task.CompletedTask; - public Task ApproveReviewAsync(string taskId) => Task.CompletedTask; + public Task ApproveReviewAsync(string taskId, string targetBranch) => Task.FromResult(null); + public Task PreviewMergeAsync(string taskId, string targetBranch) => Task.FromResult(null); + public Task MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty(), null)); public Task RejectReviewToQueueAsync(string taskId, string feedback) => Task.CompletedTask; public Task RejectReviewToIdleAsync(string taskId) => Task.CompletedTask; public Task CancelReviewAsync(string taskId) => Task.CompletedTask;