feat(ui): show mergeability and surface approve conflicts in the work console
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -339,6 +339,24 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
||||
[ObservableProperty] private string? _mergeAllDisabledReason;
|
||||
[ObservableProperty] private string? _mergeAllError;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShowMergePreviewMuted))]
|
||||
private string _mergePreviewText = "";
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShowMergePreviewMuted))]
|
||||
private bool _mergeIsClean;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShowMergePreviewMuted))]
|
||||
private bool _mergeIsConflict;
|
||||
|
||||
public bool ShowMergePreviewMuted =>
|
||||
!MergeIsClean && !MergeIsConflict && !string.IsNullOrEmpty(MergePreviewText);
|
||||
|
||||
public bool ShowSingleMerge =>
|
||||
WorktreePath != null && Task?.IsPlanningParent != true;
|
||||
|
||||
// Claude CLI stream-json parser + buffer for partial text deltas
|
||||
private readonly StreamLineFormatter _formatter = new();
|
||||
private readonly StringBuilder _claudeBuf = new();
|
||||
@@ -812,6 +830,20 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
||||
{
|
||||
await LoadChildOutcomesAsync(row.Id, ct);
|
||||
}
|
||||
|
||||
if (entity.Worktree != null
|
||||
&& entity.PlanningPhase == ClaudeDo.Data.Models.PlanningPhase.None
|
||||
&& MergeTargetBranches.Count == 0)
|
||||
{
|
||||
var targets = await _worker.GetMergeTargetsAsync(row.Id);
|
||||
if (targets != null)
|
||||
{
|
||||
MergeTargetBranches.Clear();
|
||||
foreach (var b in targets.LocalBranches) MergeTargetBranches.Add(b);
|
||||
SelectedMergeTarget = targets.DefaultBranch; // triggers OnSelectedMergeTargetChanged → preview
|
||||
}
|
||||
}
|
||||
await RefreshMergePreviewAsync();
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
@@ -1102,6 +1134,45 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
||||
catch { /* best-effort refresh */ }
|
||||
}
|
||||
|
||||
private async System.Threading.Tasks.Task RefreshMergePreviewAsync()
|
||||
{
|
||||
if (Task is null || WorktreePath is null)
|
||||
{
|
||||
MergePreviewText = ""; MergeIsClean = false; MergeIsConflict = false;
|
||||
return;
|
||||
}
|
||||
// Only probe Active worktrees; terminal states show their label instead.
|
||||
if (WorktreeStateLabel is { } label && label != "Active")
|
||||
{
|
||||
MergePreviewText = label; MergeIsClean = false; MergeIsConflict = false;
|
||||
return;
|
||||
}
|
||||
var dto = await _worker.PreviewMergeAsync(Task.Id, SelectedMergeTarget ?? "");
|
||||
var (text, clean, conflict) = MergePreviewPresenter.Describe(dto);
|
||||
MergePreviewText = text; MergeIsClean = clean; MergeIsConflict = conflict;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async System.Threading.Tasks.Task MergeAsync()
|
||||
{
|
||||
if (Task is null || WorktreePath is null || !_worker.IsConnected) return;
|
||||
try
|
||||
{
|
||||
var result = await _worker.MergeTaskAsync(Task.Id, SelectedMergeTarget ?? "", false, "Merge task");
|
||||
if (result.Status == "conflict")
|
||||
{
|
||||
var (text, _, _) = MergePreviewPresenter.Describe(
|
||||
new MergePreviewDto("conflict", result.ConflictFiles, 0));
|
||||
MergePreviewText = text; MergeIsClean = false; MergeIsConflict = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
await RefreshMergePreviewAsync();
|
||||
}
|
||||
}
|
||||
catch { /* broadcast reconciles */ }
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanOpenDiff))]
|
||||
private async System.Threading.Tasks.Task OpenDiffAsync()
|
||||
{
|
||||
@@ -1138,11 +1209,17 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
||||
|
||||
private bool CanOpenWorktree() => WorktreePath != null;
|
||||
|
||||
partial void OnSelectedMergeTargetChanged(string? value)
|
||||
{
|
||||
_ = RefreshMergePreviewAsync();
|
||||
}
|
||||
|
||||
partial void OnWorktreePathChanged(string? value)
|
||||
{
|
||||
OpenDiffCommand.NotifyCanExecuteChanged();
|
||||
OpenWorktreeCommand.NotifyCanExecuteChanged();
|
||||
NotifySessionSections();
|
||||
OnPropertyChanged(nameof(ShowSingleMerge));
|
||||
}
|
||||
|
||||
partial void OnTaskChanged(TaskRowViewModel? value) => NotifySessionSections();
|
||||
@@ -1362,10 +1439,16 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
||||
private async System.Threading.Tasks.Task ApproveReviewAsync()
|
||||
{
|
||||
if (Task is null || !_worker.IsConnected) return;
|
||||
// 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, SelectedMergeTarget ?? ""); }
|
||||
try
|
||||
{
|
||||
var result = await _worker.ApproveReviewAsync(Task.Id, SelectedMergeTarget ?? "");
|
||||
if (result?.Status == "conflict")
|
||||
{
|
||||
var (text, _, _) = MergePreviewPresenter.Describe(
|
||||
new MergePreviewDto("conflict", result.ConflictFiles, 0));
|
||||
MergePreviewText = text; MergeIsClean = false; MergeIsConflict = true;
|
||||
}
|
||||
}
|
||||
catch { /* stale review action; broadcast reconciles */ }
|
||||
}
|
||||
|
||||
|
||||
28
src/ClaudeDo.Ui/ViewModels/Islands/MergePreviewPresenter.cs
Normal file
28
src/ClaudeDo.Ui/ViewModels/Islands/MergePreviewPresenter.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Linq;
|
||||
using ClaudeDo.Ui.Services;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels.Islands;
|
||||
|
||||
/// Pure mapping from a merge-preview DTO to display text + color flags.
|
||||
public static class MergePreviewPresenter
|
||||
{
|
||||
public static (string Text, bool IsClean, bool IsConflict) Describe(MergePreviewDto? dto)
|
||||
{
|
||||
if (dto is null) return ("", false, false);
|
||||
|
||||
switch (dto.Status)
|
||||
{
|
||||
case "clean":
|
||||
var unit = dto.ChangedFileCount == 1 ? "file" : "files";
|
||||
return ($"Merges cleanly · {dto.ChangedFileCount} {unit}", true, false);
|
||||
|
||||
case "conflict":
|
||||
var names = string.Join(", ", dto.ConflictFiles.Take(3));
|
||||
var more = dto.ConflictFiles.Count > 3 ? $" (+{dto.ConflictFiles.Count - 3} more)" : "";
|
||||
return ($"Conflicts in {names}{more}", false, true);
|
||||
|
||||
default:
|
||||
return ("Mergeability unknown", false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user