diff --git a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs index 671c456e..cd7dc617 100644 --- a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs +++ b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs @@ -165,12 +165,12 @@ public partial class DetailsIslandView : UserControl return System.Threading.Tasks.Task.CompletedTask; }; - vm.Merge.ShowMergeModal = async (mergeVm) => + // Non-modal so the main window stays usable while the merge + verify runs; the task + // completes on close so the diff viewer can still react to the outcome. + vm.Merge.ShowMergeModal = (mergeVm) => { var owner = TopLevel.GetTopLevel(this) as Window; - if (owner == null) return; - var modal = new MergeModalView { DataContext = mergeVm }; - await modal.ShowDialog(owner); + return owner == null ? System.Threading.Tasks.Task.CompletedTask : MergeModalView.ShowAsync(owner, mergeVm); }; } diff --git a/src/ClaudeDo.Ui/Views/Modals/MergeModalView.axaml.cs b/src/ClaudeDo.Ui/Views/Modals/MergeModalView.axaml.cs index c52fcb25..8999811d 100644 --- a/src/ClaudeDo.Ui/Views/Modals/MergeModalView.axaml.cs +++ b/src/ClaudeDo.Ui/Views/Modals/MergeModalView.axaml.cs @@ -5,6 +5,11 @@ namespace ClaudeDo.Ui.Views.Modals; public partial class MergeModalView : Window { + /// Open merge windows by task id — the guard against starting the same merge twice. + private static readonly Dictionary OpenWindows = new(); + + private readonly TaskCompletionSource _closed = new(); + public MergeModalView() { InitializeComponent(); @@ -17,5 +22,26 @@ public partial class MergeModalView : Window }; } - + /// Shows the merge window non-modally so the owner stays usable while the merge (and its verify + /// command) runs. Completes when the window closes; a second call for a task whose window is + /// already open activates it and waits on that one instead of opening another. + public static Task ShowAsync(Window owner, MergeModalViewModel vm) + { + if (OpenWindows.TryGetValue(vm.TaskId, out var existing)) + { + if (existing.WindowState == WindowState.Minimized) + existing.WindowState = WindowState.Normal; + existing.Activate(); + return existing._closed.Task; + } + var window = new MergeModalView { DataContext = vm }; + OpenWindows[vm.TaskId] = window; + window.Closed += (_, _) => + { + OpenWindows.Remove(vm.TaskId); + window._closed.TrySetResult(); + }; + window.Show(owner); + return window._closed.Task; + } } diff --git a/src/ClaudeDo.Ui/Views/WindowDialogService.cs b/src/ClaudeDo.Ui/Views/WindowDialogService.cs index d9b1ca10..60503a3f 100644 --- a/src/ClaudeDo.Ui/Views/WindowDialogService.cs +++ b/src/ClaudeDo.Ui/Views/WindowDialogService.cs @@ -149,11 +149,7 @@ public sealed class WindowDialogService : IDialogService if (Shell is { } shell) { vm.ResolveMergeVm = shell.ResolveMergeVm; - vm.ShowMergeAction = async mergeVm => - { - var mergeDlg = new MergeModalView { DataContext = mergeVm }; - await mergeDlg.ShowDialog(_owner); - }; + vm.ShowMergeAction = mergeVm => MergeModalView.ShowAsync(_owner, mergeVm); } await dlg.ShowDialog(_owner); } diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/DiffViewerViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/DiffViewerViewModelTests.cs index 105b9400..0e21dee6 100644 --- a/tests/ClaudeDo.Ui.Tests/ViewModels/DiffViewerViewModelTests.cs +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/DiffViewerViewModelTests.cs @@ -335,4 +335,30 @@ public class DiffViewerViewModelTests Assert.Contains("planning task not found", vm.CombinedWarning); Assert.Equal("", vm.DisplayedDiff); } + + // ── Merge button: the modal is non-modal, the command spans its lifetime ── + + [Fact] + public async Task Merge_WhileMergeWindowOpen_DoesNotStartSecondMerge() + { + var worker = new FakePlanningWorker(); + var vm = new DiffViewerViewModel(null!, worker, TestSettings()); + vm.ConfigureWorktree("/wt", "main", "task-1", "Title"); + var windowClosed = new TaskCompletionSource(); + var resolved = 0; + var shown = 0; + vm.ResolveMergeVm = () => { resolved++; return new MergeModalViewModel(worker, new MergeCoordinator()); }; + vm.ShowMergeModal = _ => { shown++; return windowClosed.Task; }; + + var first = vm.MergeCommand.ExecuteAsync(null); + while (shown == 0) await Task.Delay(10); + + Assert.False(vm.MergeCommand.CanExecute(null)); + Assert.Equal(1, resolved); + Assert.Equal(1, shown); + + windowClosed.SetResult(); + await first; + Assert.True(vm.MergeCommand.CanExecute(null)); + } }