From 23a93ce0bb23d93a7956a22d25d29a9df9a6e70c Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Fri, 19 Jun 2026 13:14:51 +0200 Subject: [PATCH] fix(merge): unresolved conflicts compose to empty, not Ours (+ review nits) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code-review follow-ups before push: - MergeFile.ResultText/Compose() fell back to Ours for unresolved conflicts while the editor seeds them empty — align both on empty so the public model matches the pane and Continue can't silently auto-accept Ours. - Bound the gutter re-layout retry (was an unbounded Background re-post when the editor isn't laid out, e.g. minimized). - Pluralize the readout ('1 conflict' not '1 conflicts'). Tests updated. Ui 128 green. --- .../ViewModels/Conflicts/ConflictModels.cs | 11 +++++++---- .../ViewModels/Conflicts/ConflictResolverViewModel.cs | 2 +- .../Views/Conflicts/ConflictResolverView.axaml.cs | 7 ++++++- .../ViewModels/ConflictResolverViewModelTests.cs | 8 ++++---- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictModels.cs b/src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictModels.cs index 7212e24..e4db884 100644 --- a/src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictModels.cs +++ b/src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictModels.cs @@ -72,9 +72,11 @@ public sealed class MergeFile /// A binary file can't be resolved in-app; a text file is done once every block is resolved. public bool AllResolved => !IsBinary && Conflicts.All(c => c.IsResolved); - /// Reassemble the file: stable text verbatim, each conflict replaced by its resolution. + /// Reassemble the file: stable text verbatim, each conflict replaced by its resolution + /// (empty when unresolved — the same "empty start" the editor shows; Continue is gated on + /// so an unresolved conflict never actually reaches here). public string Compose() => string.Concat( - Segments.Select(s => s.IsConflict ? (s.Conflict!.Resolution ?? s.Conflict.Ours) : s.StableText)); + Segments.Select(s => s.IsConflict ? (s.Conflict!.Resolution ?? "") : s.StableText)); /// Left pane document: stable regions verbatim, conflict regions show Ours text. public string OursText => string.Concat( @@ -84,7 +86,8 @@ public sealed class MergeFile public string TheirsText => string.Concat( Segments.Select(s => s.IsConflict ? s.Conflict!.Theirs : s.StableText)); - /// Middle (result) pane document: stable regions verbatim, conflict regions show Resolution if set, else Ours. + /// Middle (result) pane document: stable regions verbatim, conflict regions show the + /// chosen Resolution, or empty when unresolved (the editor builds each conflict up from empty). public string ResultText => string.Concat( - Segments.Select(s => s.IsConflict ? (s.Conflict!.Resolution ?? s.Conflict.Ours) : s.StableText)); + Segments.Select(s => s.IsConflict ? (s.Conflict!.Resolution ?? "") : s.StableText)); } diff --git a/src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictResolverViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictResolverViewModel.cs index fe6b3e3..a4018a8 100644 --- a/src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictResolverViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictResolverViewModel.cs @@ -81,7 +81,7 @@ public sealed partial class ConflictResolverViewModel : ObservableObject if (ActiveFile is null || ActiveFile.Conflicts.Count == 0) return "No text conflicts"; var count = ActiveFile.Conflicts.Count; var resolved = ActiveFile.Conflicts.Count(c => c.IsResolved); - return $"{count} conflicts · {resolved} resolved"; + return $"{count} {(count == 1 ? "conflict" : "conflicts")} · {resolved} resolved"; } } diff --git a/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml.cs b/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml.cs index 6bda13a..2e33714 100644 --- a/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml.cs +++ b/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml.cs @@ -40,6 +40,7 @@ public partial class ConflictResolverView : Window private bool _applyingAccept; private bool _syncing; private bool _gutterPending; + private int _gutterRetries; public ConflictResolverView() { @@ -109,6 +110,7 @@ public partial class ConflictResolverView : Window { if (_vm is null) return; _rebuilding = true; + _gutterRetries = 0; // fresh retry budget for this file's gutter layout try { ClearGutters(); @@ -257,9 +259,12 @@ public partial class ConflictResolverView : Window var tv = ResultEditor.TextArea.TextView; if (!tv.VisualLinesValid) { - QueueGutters(); + // Retry until the editor is laid out, but bounded so a never-laid-out editor + // (e.g. minimized window) can't busy-loop the dispatcher. + if (_gutterRetries++ < 40) QueueGutters(); return; } + _gutterRetries = 0; var doc = ResultEditor.Document; foreach (var region in _resultRegions) diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolverViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolverViewModelTests.cs index 6839823..2d295b6 100644 --- a/tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolverViewModelTests.cs +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolverViewModelTests.cs @@ -238,7 +238,7 @@ public class ConflictResolverViewModelTests Assert.Equal("a\no\nz\n", file.OursText); Assert.Equal("a\nt\nz\n", file.TheirsText); - Assert.Equal("a\no\nz\n", file.ResultText); // unresolved seeds Ours + Assert.Equal("a\nz\n", file.ResultText); // unresolved conflicts start empty // After resolving: ResultText reflects the resolution file.Conflicts[0].Resolution = "r\n"; @@ -286,7 +286,7 @@ public class ConflictResolverViewModelTests // Switch to file B vm.SelectFileCommand.Execute(vm.Files[1]); Assert.Equal("b.cs", vm.ActiveFile!.Path); - Assert.Equal("ours-b\n", vm.ActiveResultText); // unresolved seeds Ours + Assert.Equal("", vm.ActiveResultText); // unresolved conflicts start empty // Switch back to file A vm.SelectFileCommand.Execute(vm.Files[0]); @@ -303,11 +303,11 @@ public class ConflictResolverViewModelTests await vm.OpenAsync("main"); // 1 conflict, 0 resolved - Assert.Equal("1 conflicts · 0 resolved", vm.PositionText); + Assert.Equal("1 conflict · 0 resolved", vm.PositionText); vm.Current!.AcceptOursCommand.Execute(null); // 1 conflict, 1 resolved - Assert.Equal("1 conflicts · 1 resolved", vm.PositionText); + Assert.Equal("1 conflict · 1 resolved", vm.PositionText); } }