diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json
index 2bf19ba6..ddbfc14a 100644
--- a/src/ClaudeDo.Localization/locales/de.json
+++ b/src/ClaudeDo.Localization/locales/de.json
@@ -539,7 +539,8 @@
"removeWorktree": "Worktree nach dem Mergen entfernen",
"commitMessage": "Commit-Nachricht",
"conflictedFiles": "Konfliktdateien:",
- "stashAndMerge": "Änderungen stashen und mergen"
+ "stashAndMerge": "Änderungen stashen und mergen",
+ "resolveConflicts": "Konflikte lösen"
},
"diff": {
"title": "DIFF",
diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json
index 928398a3..36262cc9 100644
--- a/src/ClaudeDo.Localization/locales/en.json
+++ b/src/ClaudeDo.Localization/locales/en.json
@@ -539,7 +539,8 @@
"removeWorktree": "Remove worktree after merge",
"commitMessage": "Commit message",
"conflictedFiles": "Conflicted files:",
- "stashAndMerge": "Stash changes and merge"
+ "stashAndMerge": "Stash changes and merge",
+ "resolveConflicts": "Resolve conflicts"
},
"diff": {
"title": "DIFF",
diff --git a/src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictResolverViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictResolverViewModel.cs
index b17fbe5a..1226be2d 100644
--- a/src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictResolverViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictResolverViewModel.cs
@@ -122,6 +122,9 @@ public sealed partial class ConflictResolverViewModel : ObservableObject
public string TaskId => _taskId;
public Action? CloseRequested { get; set; }
+ /// True once the merge was continued or aborted; a window closed before that must abort, or the repo stays mid-merge with an autostash pending.
+ public bool IsSettled { get; private set; }
+
/// Raised when the current conflict changes so the view can reload its editors.
public event Action? CurrentChanged;
@@ -277,13 +280,17 @@ public sealed partial class ConflictResolverViewModel : ObservableObject
// Hand back to the orchestrator: it commits this subtask and drains the rest.
// A later subtask conflict re-opens this editor via the PlanningMergeConflict broadcast.
await _worker.ContinuePlanningMergeAsync(_planningParentId);
+ IsSettled = true;
CloseRequested?.Invoke();
return;
}
var result = await _worker.ContinueConflictMergeAsync(_taskId);
if (string.Equals(result.Status, "merged", StringComparison.Ordinal))
+ {
+ IsSettled = true;
CloseRequested?.Invoke();
+ }
else
Error = result.ErrorMessage ?? "Conflicts not fully resolved — review and retry.";
}
@@ -297,6 +304,7 @@ public sealed partial class ConflictResolverViewModel : ObservableObject
[RelayCommand]
private async Task AbortAsync()
{
+ IsSettled = true;
IsBusy = true;
try
{
diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs
index 5860d83c..f307f128 100644
--- a/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs
@@ -16,11 +16,11 @@ public sealed partial class MergeModalViewModel : ViewModelBase
public ObservableCollection Branches { get; } = new();
- [ObservableProperty][NotifyCanExecuteChangedFor(nameof(SubmitCommand))] private string? _selectedBranch;
+ [ObservableProperty][NotifyCanExecuteChangedFor(nameof(SubmitCommand), nameof(ResolveConflictsCommand))] private string? _selectedBranch;
[ObservableProperty] private bool _removeWorktree = true;
[ObservableProperty] private string _commitMessage = "";
- [ObservableProperty][NotifyCanExecuteChangedFor(nameof(SubmitCommand))] private bool _isBusy;
+ [ObservableProperty][NotifyCanExecuteChangedFor(nameof(SubmitCommand), nameof(ResolveConflictsCommand))] private bool _isBusy;
/// What the pending merge is doing right now, fed by the worker's MergeProgress broadcast.
/// A merge whose list has a verify command can occupy this call for minutes — without this the
@@ -37,6 +37,11 @@ public sealed partial class MergeModalViewModel : ViewModelBase
[ObservableProperty] private bool _showStashRetry;
[ObservableProperty] private IReadOnlyList _conflictFiles = Array.Empty();
+ /// The merge preview for the selected branch predicts a conflict — offers the resolver up front.
+ [ObservableProperty] private bool _previewHasConflict;
+
+ private bool _targetDirty;
+
public Action? CloseAction { get; set; }
/// True once a merge has succeeded — lets the caller (e.g. the diff window)
@@ -79,6 +84,7 @@ public sealed partial class MergeModalViewModel : ViewModelBase
// Known dirty before anyone clicks Merge — offer the stash retry right away instead
// of making the user hit the "dirty_target" refusal first.
+ _targetDirty = targets.TargetDirty;
if (targets.TargetDirty)
{
WarningMessage = Loc.T("vm.merge.targetDirty");
@@ -92,6 +98,29 @@ public sealed partial class MergeModalViewModel : ViewModelBase
finally { IsBusy = false; }
}
+ partial void OnSelectedBranchChanged(string? value) => _ = RefreshPreviewAsync(value);
+
+ private async Task RefreshPreviewAsync(string? branch)
+ {
+ PreviewHasConflict = false;
+ if (string.IsNullOrWhiteSpace(branch) || string.IsNullOrEmpty(TaskId)) return;
+ try
+ {
+ var dto = await _worker.PreviewMergeAsync(TaskId, branch);
+ if (SelectedBranch == branch) PreviewHasConflict = dto?.ConflictFiles.Count > 0;
+ }
+ catch { /* preview is a hint only; Merge still reports the real conflict */ }
+ }
+
+ /// Hands the predicted conflict straight to the resolver; stashes a dirty target, as the warning above announces.
+ [RelayCommand(CanExecute = nameof(CanSubmit))]
+ private async Task ResolveConflictsAsync()
+ {
+ RoutedToResolver = true;
+ CloseAction?.Invoke();
+ await _merge.ResolveConflictAsync(TaskId, SelectedBranch!, _targetDirty);
+ }
+
private bool CanSubmit() =>
!IsBusy && !HasConflict && !string.IsNullOrWhiteSpace(SelectedBranch);
diff --git a/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml.cs b/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml.cs
index 47160d69..738d3e20 100644
--- a/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml.cs
+++ b/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml.cs
@@ -42,6 +42,13 @@ public partial class ConflictResolverView : Window
InitializeComponent();
}
+ /// Alt+F4 bypasses the Abort-bound close button; abort here so the merge and its autostash never stay pending.
+ protected override void OnClosed(EventArgs e)
+ {
+ base.OnClosed(e);
+ if (_vm is { IsSettled: false }) _vm.AbortCommand.Execute(null);
+ }
+
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
diff --git a/src/ClaudeDo.Ui/Views/Modals/MergeModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/MergeModalView.axaml
index 1cea2654..47c396b7 100644
--- a/src/ClaudeDo.Ui/Views/Modals/MergeModalView.axaml
+++ b/src/ClaudeDo.Ui/Views/Modals/MergeModalView.axaml
@@ -74,10 +74,16 @@
TextWrapping="Wrap"
IsVisible="{Binding WarningMessage, Converter={x:Static ObjectConverters.IsNotNull}}" />
-
+
+
+
+